Skip to content

Latest commit

 

History

80 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Asistente de Piso — IA aplicada a retail real

Asistente de Piso

IA para el piso de venta que cita la página exacta del manual, declara su certeza y dice «no sé» cuando no hay evidencia.

Demo pública Cómo funciona MIT

Un solo archivo HTML Búsqueda local Sin backend Sin telemetría Arnés interno: 232 pruebas en cada push Probado con 30 manuales reales

Asistente conversacional de una sola página para el personal de piso de una tienda: responde dudas de montaje y estándar de exhibición desde el celular, parado frente al mueble, sin instalar nada y sin cuenta.

De dónde salió: soy promotor visual en Liverpool (Guadalajara) y llegué a tener seis secciones a mi cargo, cada una con su propio manual de campaña. Nadie abre un manual de 40 páginas frente al mueble, así que las dudas se resuelven preguntando o adivinando. Este asistente nació de ese problema, visto todos los días desde el piso. Más contexto en mi perfil.

Demo con conocimiento 100 % sintético. El cliente (Mercadep), las marcas (MarcaDemoA–MarcaDemoL, y las cinco de la lámina de logos del manual demo), los mundos, los porcentajes de piso y los números de manual son inventados. Ningún manual, marca, medida ni dato operativo de un cliente real vive en este repositorio ni en su historial.

▶ Abrir el demo · sin API key funciona en modo manual; con tu propia key (Google AI Studio u OpenAI) activa el modo razonado.

Probado con 30 manuales reales

Lo probé con 30 manuales reales de campaña de una tienda departamental, cargados a la vez. Los PDF no están en este repositorio. Todo se midió en modo manual, sin API y sin red, con preguntas sacadas del texto de los propios manuales y la página de cada respuesta verificada a mano:

Resultado
186 preguntas de piso: la lámina que contesta sale entre las 3 primeras 166/186 (89 %)
… y en primer lugar 137/186 (74 %)
Preguntas que se quedan sin ninguna respuesta 0
Las mismas 186, escritas con faltas de celular («donde ban las sandalas»): sin respuesta 6
118 preguntas hechas en un manual que no las contiene: dice «no está» 117/118
30 preguntas trampa que ningún manual contesta («¿qué hago si se va la luz?»): ninguna sale con tarjeta 30/30
Fragmentos o cifras sacados de otro manual 0

En primer lugar eran 128. Subió a 137 cuando los rótulos sueltos de los dibujos dejaron de ganar por cortos y las palabras que escribió el asesor empezaron a pesar más que sus sinónimos. Los «no está», las trampas y los avisos quedaron igual.

El último renglón es el que más cuido. La misma pregunta tiene cifras distintas según la sección, y un dato de otra sección suena cierto aunque esté mal.

Y el modo razonado, con los mismos manuales

El modo con API key también está medido, con eval/modo-ia.mjs dentro de la app real: 110 preguntas de esos 30 manuales (60 de dato, dos por manual; 20 que su manual no contesta y 30 trampas). A Google solo salen los fragmentos que la búsqueda elige para cada pregunta. Esos números son del motor clásico; el agente lector (abajo) está sin medir.

Resultado
Trae el dato y cita una página donde está 55/60
Dice «no está» en las que no están y en las trampas 49/50
Respuestas con aviso de «no pude verificar» 3 (las tres, falsas alarmas: «definir», «típicos», «smartwatch»)
Primer texto en pantalla, mediana / 95 % 2.0 s / 19.5 s

Durante toda la prueba Google tuvo gemini-3.5-flash saturado, así que 109 de 110 las contestó el respaldo automático, gemini-3.5-flash-lite. Es lo que habría visto un asesor ese día. La trampa que falló es la de siempre: «¿qué hago si se va la luz?» contestó con la luz como elemento de equilibrio de una exhibición. Con el manual demo (eval/preguntas-demo.json, 36 preguntas) sale 22/24 y 12/12.

También probé quitarle las seis etapas para que la respuesta saliera mientras se escribe. El primer texto bajó a 0.9 s, pero acertó 51/60 en vez de 55: dos veces dijo «no está» cuando sí estaba. La regla era no perder más de un acierto a cambio de velocidad, así que no entró.

Los números de los 30 manuales son de antes de cambiar el orden de las láminas (arriba). Ese cambio no movió cuántas preguntas llevan el dato al modelo (182 de 186), pero sí el orden en que llega. Con el manual demo ya está medido de nuevo (26-sep, también todo por el respaldo 3.5 Flash-Lite): 22/24 y 12/12, igual que antes, con el primer texto en 2.0 s de mediana. De los dos que fallan, uno es del modelo y otro de la búsqueda. «La barra se ve muy llena» sí trae la lámina de SATURACIÓN, en primer lugar, pero el modelo contestó con la de alineación. «¿Qué reviso antes de que llegue la regional?» no traía nada, porque el CHECK LIST nunca escribe «revisar». Ya lo trae: el diccionario lleva «reviso» al CHECK LIST, y de las 36 preguntas del demo es la única cuyo contexto cambia. Lo que conteste el modelo con eso está sin medir. Falta la medición con los 30 manuales.

Hay una segunda prueba de velocidad, sin tocar la app: eval/modo-ia.mjs --variante lectura-corta cambia las seis etapas por un solo paso («ubica la página y el rótulo del dato, o di que no está») antes de responder. Con el manual demo empata en aciertos (22/24, 12/12) y con el mismo modelo tarda la mitad (1.0 s contra 2.0 s de mediana). Pero una respuesta nueva salió contradictoria: dice «el manual no especifica» y luego da la regla. Para eso está --variante lectura-corta-2, que agrega una línea: si el contexto trae la regla, se contesta con ella. Está sin medir. Ninguna de las dos entra a la app hasta medirlas con los 30 manuales reales.

Cómo funciona, en 13 segundos

Animación: el manual entra al celular, se parte en láminas, llega una pregunta con faltas y sube la lámina que contesta, con su página

Lee tu manual → lo parte en láminas → preguntas como hablas → responde con la página y la lámina. La pregunta, la lámina que gana, el texto y el dibujo salen del motor real corriendo sobre el manual de ejemplo: ver en MP4.

Qué resuelve

El manual de montaje de una campaña son decenas de páginas en PDF. Nadie las carga al piso, así que las dudas se resuelven preguntando —si hay a quién— o adivinando. Este asistente pone ese conocimiento a un toque de distancia, en el lenguaje con el que realmente se pregunta: "¿a qué altura va el sensor?", no "criterios de colocación de dispositivo EAS".

Dos modos, y el de abajo es el interesante

Modo manual (sin API key) Modo razonado (con API key)
Qué hace Busca en el manual y entrega los fragmentos que coinciden, tal cual, con su página y su lámina Dos motores, a elegir en Ajustes. Agente lector: la IA lee el manual con herramientas —páginas, láminas, búsqueda— y contesta citando lo que leyó. Clásico: la búsqueda local elige los fragmentos y el modelo responde en 6 etapas
Dónde corre Entero en tu dispositivo, incluida la lectura del PDF y el recorte de figuras Las herramientas corren en tu dispositivo; la lectura y la respuesta, en el proveedor que elijas
Sale a la red No. Ni una petición Sí. Al preparar un manual para el modo IA, cada página (texto e imagen) va una vez al proveedor. En cada pregunta van el mapa del manual y las páginas que se leen (agente) o los fragmentos que eligió la búsqueda (clásico)
Qué cuesta Nada Tu propia key y tus propios tokens. Preparar un manual de 25 láminas con Gemini Flash-Lite: unos 5 centavos de dólar, una vez

El modo manual existe porque un demo que primero te pide una API key no es un demo. Pero sobre todo existe porque declara su límite en vez de disimularlo: dice "sin modelo conectado, nadie interpretó esto" y entrega la fuente. Y si nada del manual coincide con la pregunta, lo dice — no rellena con la sección más cercana.

El agente lector: la IA lee el manual, el código la vigila

En el motor clásico la búsqueda local decide qué fragmentos ve el modelo, y le da órdenes según lo que encontró. Cuando la búsqueda falla —«contemporáneo» no llega a una lámina que dice «Contempo», «Trevsik» no llega a «Tresvik», las marcas vienen en logos que el PDF no trae como texto—, el modelo nunca ve la respuesta y contesta «no está». Eso pasó en una conversación real del piso.

El agente invierte los papeles:

  1. Al preparar el manual, la IA lee cada página una sola vez (texto e imagen) y escribe su ficha: de qué trata, qué marcas y mundos nombra, lo que se lee en la imagen y no está en el texto, abreviaturas («Contempo = contemporáneo») y cómo lo preguntaría el piso. Se guarda en el teléfono. La ficha ubica, no contesta: en la búsqueda, un acierto en la ficha se cambia por los fragmentos reales de esa página. Lo único de la ficha que puede sostener un dato es lo que la IA copió literal de la imagen, y va rotulado como tal.
  2. En cada pregunta, el modelo recibe el mapa del manual (una línea por página, de la ficha), el glosario de la sección y dos páginas que la búsqueda local le adelanta. Con herramientas que corren en el teléfono decide qué leer: leer_paginas, ver_lamina (la imagen de la página), buscar y, con varios manuales, buscar_en_otras_secciones, que solo dice dónde está algo y nunca devuelve su contenido.
  3. Contesta con lo que entendió, lo que corrigió de lo escrito y la evidencia literal con su página. En pantalla sale «Entendí Tresvik (escribiste «Trevsik»)», qué páginas leyó y qué lámina miró, y botones para seguir: «¿Quisiste decir…?», o las preguntas que sí contestan las láminas que leyó.

El código es el guardián, no el que decide. Pone tope a las vueltas (4, la última sin herramientas), a las páginas (8), a las láminas (2) y a las llamadas por vuelta (6). Valida cada argumento: una página que no existe o una herramienta inventada vuelven al modelo como error, nunca como excepción. Comprueba que cada cita esté literal en una página que el agente leyó; si no, la certeza baja de ALTA a MEDIA y lo dice. Y si el ciclo falla por lo que sea —error del proveedor, respuesta vacía—, la misma pregunta la contesta el motor clásico. Con Gemini 3, las firmas de pensamiento que acompañan cada llamada a herramienta se devuelven intactas en la siguiente vuelta, como exige la API.

Estado: probado de punta a punta contra un Gemini y un OpenAI simulados (eval/agente-simulado.mjs, que también corre en el CI: protocolo, topes, citas inventadas, imagen rechazada, caída al clásico) y con 37 pruebas nuevas en el arnés. Sin medir contra un modelo real: por eso el motor de fábrica sigue siendo el clásico. Para medirlo con el manual demo, que ahora trae una lámina con las marcas solo como imagen:

node eval/modo-ia.mjs --motor clasico --etiqueta clasico
node eval/modo-ia.mjs --motor agente  --etiqueta agente
node eval/modo-ia.mjs --resumen

El set del demo suma ocho preguntas de las que fallaban en el piso: listas («¿qué marcas van en el mundo contemporáneo?»), abreviaturas, marcas con errata, datos que solo están en una imagen, un diagrama y dos conversaciones de varios turnos.

Con muchos manuales, la sección la elige la pregunta

Con varios manuales cargados y el selector en «Todos», cada pregunta se buscaba en todos a la vez. El agente no corría (lee una sola sección), el modo manual mezclaba tarjetas de varias secciones y hasta del manual interno de demostración, y el clásico le mandaba al modelo fragmentos de tres secciones con un aviso. Ahora la app elige la sección para esa pregunta, sin tocar el selector:

  1. la que eligió el asesor arriba, si eligió;
  2. la que la pregunta nombra («¿qué va en el POS de muebles?»);
  3. la de la conversación, si es un seguimiento («¿y sandalias?») o si esa sección responde con evidencia sólida;
  4. la única que responde con evidencia clara;
  5. si dos o más responden igual de bien («¿cuánto pasillo dejo?» sale igual en tres manuales), no adivina: pregunta «¿en cuál estás?» con un botón por sección, sin gastar una llamada.

La respuesta dice de dónde salió y por qué («📕 Busqué en … (por tu pregunta)»), con un botón a la otra sección si también tenía con qué responder.

Medido con 5 manuales reales cargados a la vez y 102 preguntas de piso (fuera del repo), haciendo las preguntas de cada sección en orden y sin elegir sección:

Antes Ahora
Sección elegida bien — (buscaba en todas) 83/86 · 2 toques de botón en 102 preguntas
Clásico: preguntas con fragmentos de otra sección 64 3
Modo manual: tarjetas de otra sección 41 3
Modo manual: tarjetas del manual interno de demostración 9 0
Modo manual: la página correcta entre las 3 tarjetas 64/86 70/86
Agente: el dato está en lo que lee no corría 80/86

La cifra tiene que ir pegada a lo que dice el manual

La verificación comprobaba que cada cifra de la respuesta existiera en lo consultado, no a qué iba pegada. En la tabla de participación de una sección de calzado cada columna trae su porcentaje, y una respuesta que le daba a SNEAKERS el de la columna TENIS CASUAL pasaba limpia, porque esa cifra sí estaba en lo consultado. Ahora cada cifra dicha se compara, por su vecindad (el título de su fragmento y su tramo de renglón), con las demás cifras de la misma unidad. Solo se marca con evidencia de estructura: otra cifra pegada por título o rótulo que explica todo lo que explica la dicha y algo más. Con manuales reales: 0 de 21 respuestas correctas marcadas, 60 de 77 con la cifra cambiada marcadas.

Y la certeza ya no puede quedar en ALTA con un dato que la app no pudo comprobar (una cifra, un nombre o una página sin respaldo, o una cifra mal atada): baja a MEDIA, el aviso lo dice y se enseña la lámina —o la página entera— donde está la cifra.

El caso de esa tabla tenía además un error de lectura: el encabezado «TENIS / CASUAL» venía en dos renglones y su porcentaje quedaba titulado «CASUAL», igual que la columna vecina. La lectura del PDF une ahora los títulos de tabla apilados. Un manual guardado con la lectura anterior se marca en la lista («↻ vuelve a elegir el PDF») y se relee al volver a elegirlo, sin perder su ficha ni las descripciones de láminas.

Medir con tus manuales, sin compartir la key

Los manuales reales no pueden estar en el repositorio y la key es de quien la usa, así que la medición corre en la propia app. En Manuales → «🧪 Medir el modo IA con un examen» se elige un examen (un JSON con preguntas de piso, la sección de cada una, lo que debe traer la respuesta y su página). La app prepara la ficha de las secciones que falten, pregunta con el motor clásico y con el agente como lo haría el asesor, califica con la misma regla que eval/modo-ia.mjs y al final comparte o descarga un archivo de resultados, que no incluye la key. Guarda el avance: si se acaba la cuota del día, al volver a abrir el mismo examen sigue donde se quedó. Las preguntas del examen no entran al tablero del equipo.

{"examen": "mis manuales", "preguntas": [
  {"seccion": "140 CASUAL HOMBRE", "q": "¿a qué altura va el sensor?", "tipo": "dato", "k": ["15 cm"], "p": [6]},
  {"seccion": "140 CASUAL HOMBRE", "q": "¿qué hago si se va la luz?", "tipo": "trampa"},
  {"seccion": "140 CASUAL HOMBRE", "q": "¿y en el clásico?", "turnos": ["¿qué marcas van en el mundo contemporáneo?"], "tipo": "dato", "k": ["Brastow", "Ondera"], "minK": 2}
]}

tipo es dato (debe traer alguna de k —o minK de ellas— y citar una página de p), no-esta o trampa (debe decir que el manual no lo especifica) u otra (debe decir en qué sección, otra, está).

En el panel se eligen los motores —clásico, agente y modo manual, que se mide sin key— y si se mide «como el asesor: sin elegir sección». En ese modo la app elige la sección de cada pregunta y se califica también si la eligió bien; si hay empate, se toca la sección del examen y se cuenta el toque. El resumen dice además cuántas veces avisó de una cifra mal atada y cuántas de esas en respuestas que estaban bien (las falsas alarmas), y cuántas veces bajó la certeza.

Medido así el modo manual, con los 5 manuales reales y las 102 preguntas: 56/86 datos con la sección elegida y 52/86 sin elegirla, preguntando cada una suelta (sección bien 92/102, con 20 toques).

Las preguntas de verdad hacen mejor examen que las escritas leyendo el manual, que usan sus palabras. En el Tablero → «⬇ Preguntas para examen» se exportan las dudas del equipo con el formato del examen, sin repetir, con lo que la app encontró y la nota de cada 👎 como pista. Falta anotar en cada una el dato y la página esperados.

Un instrumento que no se rompe a media medición. Antes de medir, «🩺 Chequeo antes de medir» revisa las secciones, la lectura de los PDF y las fichas, hace una sola llamada para saber si la key sirve y si queda cuota, y estima el costo de la tanda. Durante la medición:

  • el límite por minuto del plan gratis se espera lo que dice el proveedor, con el mismo modelo;
  • la cuota del día para la tanda sin guardar errores como resultados, y al día siguiente sigue donde se quedó;
  • midiendo nunca se cambia de modelo: si el elegido está saturado, la fila es un error y no la respuesta de otro;
  • una respuesta del agente cortada por tokens se repite una vez con más margen;
  • cada fila guarda el modelo que de verdad contestó, la versión de la app y la de lectura;
  • cada falla se clasifica por capa: sección, búsqueda (el dato no llegó), modelo (llegó y contestó otra cosa), página citada, error del proveedor.

Medir sin teléfono: el banco y el comparador

El mismo examen, la misma calificación y el mismo archivo de resultados, corriendo en un Chromium sin ventana. Los manuales se leen de una carpeta fuera del repo y la key de GEMINI_API_KEY (el modo manual no la necesita):

node eval/banco.mjs --manuales ~/manuales --examen ~/examen.json --motores manual
GEMINI_API_KEY=… node eval/banco.mjs --manuales ~/manuales --examen ~/examen.json \
    --motores clasico,agente --etiqueta base
node eval/comparar.mjs eval/resultados/base__….json eval/resultados/cambio__….json

--libre mide «como el asesor» y --continuar <resultado> retoma una tanda que cortó la cuota. comparar.mjs dice, pregunta por pregunta, qué se arregló, qué se rompió (y sale con error si algo se rompió) y en qué capa siguen las fallas. Con el modo manual y los 5 manuales reales, el banco reproduce la medición de la app: 56/86 con sección elegida, 52/86 sin elegirla.

La app aprende del piso: caminos, nunca respuestas

El piso no habla como el manual. Dice «burros» donde la lámina dice «percheros», o pregunta por algo que está en la pág. 7 con palabras que la pág. 7 no usa. La app aprende esos caminos: una palabra del piso que lleva a una palabra del manual, y una forma de preguntar que lleva a una página. El dato sigue saliendo siempre del manual, con su página. No se aprende una sola respuesta.

Aprende de cuatro cosas que el asesor ya hace:

  • El 👎 pide la lámina. «¿En qué página estaba?», con las páginas más probables como botones y un número para escribir la que sea. Un toque deja un atajo y abre esa lámina. Si la pregunta traía palabras que ese manual no usa, quedan emparejadas, por confirmar, con el título de esa página. En modo manual, un «nada coincide» también ofrece decir dónde estaba. Y «No está en el manual» lo anota como hueco real en el Tablero.
  • «✓ Sí, eso». Cuando la app dice «lo encontré como…» o el agente dice «Entendí…», un toque lo confirma.
  • La reformulación. Una pregunta que no llegó, seguida en menos de 3 minutos por otra parecida que sí llegó. Lo que cambió entre una y otra es cómo lo dice el manual.
  • La IA propone (con key). Mira las palabras del piso que siguen sin resolver y los títulos del manual, y propone equivalencias. Se hace al terminar de preparar un manual o con «✨ Que la IA proponga» en el Tablero. Lo propuesto no se usa hasta que alguien lo confirma.

Con candados, porque aprender mal empeora lo que ya funcionaba:

  • cada palabra es de una sección;
  • se activa con dos confirmaciones de consultas distintas, así que una errata aislada o un toque equivocado no se aprende;
  • nunca cifras;
  • la palabra del manual tiene que existir en ese manual;
  • lo que ya dice el diccionario no se duplica;
  • hay un tope por sección.

Lo aprendido entra a la búsqueda como una palabra más del diccionario: pesa igual y ayuda a llegar, pero no cuenta como palabra escrita. El atajo solo reordena lo que ya llegó por sus palabras; no mete una página sin evidencia. Cada respuesta dice qué usó: «📚 Usé lo que aprendió el piso: «burros» = «percheros»».

En el Tablero, «Lo que aprendió del piso» enseña las palabras activas, las que están por confirmar y las propuestas de la IA, con ✓ para confirmar y 🗑 para borrar. Desde ahí se saca todo con «⬇ Vocabulario del piso»: palabras y páginas, sin preguntas. El archivo sirve para revisarlo y, si el banco dice que mejora, subirlo al diccionario del código para todos los teléfonos. «⬆ Cargar vocabulario» lo pasa a otro teléfono con los mismos candados. «Aprender del piso» se apaga en Ajustes.

Midiendo no se aprende nada, y por defecto tampoco se usa lo aprendido. La medición es del código, no de la memoria de un teléfono. Para medir el efecto está la casilla «Con lo aprendido del piso» en la app, o --aprendido vocabulario.json en el banco; el resultado lo dice y comparar.mjs lo marca. Así, «¿sirvió aprender?» se contesta con el mismo examen, sin y con.

Esto reemplaza a la vieja «memoria de aprendizaje» de 👍/👎, que pegaba al prompt las preguntas que salieron mal. No movía nada medible y se colaba en la medición. Se borró, y sus datos se borran del teléfono al abrir la versión nueva. Las notas del 👎 ya viajaban en el Tablero.

Qué sigue: la hoja de ruta dice cómo trabajamos (nada mejora sin número), dónde estamos por capa y el orden de lo que viene: la línea base con modelo real y, con ella, medir cuánto suma lo aprendido del piso.

Cómo está hecho

  • Un solo archivo index.html. Sin build, sin bundler, sin backend propio, sin servidor que mantener. Se publica como archivo estático y se abre desde un link. Es a propósito: por qué, y cómo se recorre.
  • Conocimiento embebido y estructurado, con un diccionario de sinónimos y alias por término — la gente no pregunta con el vocabulario del manual. "Acomodar" tiene que encontrar "exhibir", "clasificar" y "mercadear".
  • Retrieval léxico local con BM25: se extraen las palabras de la pregunta, se expanden con sus sinónimos —que pesan menos que la palabra que el usuario escribió de verdad— y se puntúan los fragmentos por relevancia, con peso extra para las reglas [MANDATORY], los conflictos documentados y, cuando la pregunta pide una cantidad, para los fragmentos que traen cifras.
  • Conflictos documentados como feature. Cuando dos manuales se contradicen (el entallado lleva 4, 5 o 6 piezas según cuál leas), el sistema tiene prohibido responder "el manual no especifica": cita el conflicto y la regla general.
  • Cuando la búsqueda no encuentra nada, se dice. El contexto llega encabezado por un aviso de que no hubo coincidencias y con la orden de contestar "el manual no especifica", en vez de entregar fragmentos sueltos bajo la instrucción de "responde solo con esto" — que es obedecer y componer una regla que nadie escribió. En ese caso tampoco se guarda evidencia: no puede salir una lámina debajo de una respuesta que el manual no sostiene. Son tres niveles, no dos, y eso salió de medir: a escala real «¿cómo acomodo las tallas?» —una pregunta central del piso— deja la misma huella que «¿a qué hora abre la tienda?». Con evidencia sólida se responde normal; con evidencia débil el contexto va igual pero avisando de que la coincidencia es floja, y decide el modelo, que sabe leer si esos fragmentos vienen al caso; sin ninguna coincidencia no se finge nada. Cortar en seco el caso intermedio contesta "no lo especifica" a preguntas que el manual sí contesta, y eso vacía la herramienta más rápido que un contexto de más.
  • Una sección a la vez. El asesor elige con qué manual está trabajando y solo ese se consulta; queda un «todos» explícito para comparar. No es un lujo de interfaz: medido con cinco manuales reales de la misma plantilla, «¿cuánto pasillo dejo?» armaba la respuesta con fragmentos de los cinco y enseñaba tres láminas de tres manuales distintos. El dato salía bien y citado a la página de un manual que no era el suyo, que es peor que un dato inventado: el asesor va a esa página y no está.
  • La identidad la pone el manual. El nombre de la sección se saca del propio documento —«271 CASUAL», «436 COLECCIONES BEBÉS»— y con él se presenta el asistente, se rotulan las respuestas y se generan accesos rápidos con los títulos de sus propias láminas. Antes se presentaba como especialista de Hombres delante de un manual de Bebés, y el filtro de tema —vocabulario de Hombres— dejaba fuera preguntas tan de piso como «¿qué se debe limpiar?».
  • Una sola fuente por respuesta. Con un PDF cargado, el conocimiento interno no entra al contexto. Traía cifras propias —90 cm de pasillo, 40%, 50%— que el modelo citaba como si fueran del manual del asesor, y la verificación las daba por buenas porque estaban en el contexto: para quien lee en el piso, eso es exactamente un dato inventado. Lo que el PDF no cubra se consulta aparte, con un botón, y la respuesta viene rotulada como lo que es.
  • Cada respuesta declara su certeza —ALTA, MEDIA o GAP— en una última línea que se lee automáticamente, no se muestra, y decide el distintivo del mensaje y si se enseña lámina o no.

Por qué todo vive en un solo index.html

Son unas 8,200 líneas en un archivo, y a primera vista parece desorden. Lo decidí así por cómo se usa la app en el piso:

  • Tiene que abrir sin señal. En el piso la señal va y viene, y la prueba de la junta es poner el celular en modo avión y seguir preguntando. El service worker (sw.js) guarda la página, las tres librerías del CDN y las fuentes. Como todo el código viaja en un solo archivo, el celular nunca queda con el JavaScript nuevo y el HTML viejo.
  • Se publica sin compilar. GitHub Pages sirve el archivo tal cual: no hay build que se rompa, ni node_modules que caduquen, ni servidor que pagar. Lo que está en main es lo que abre el asesor.
  • Se puede revisar entero. Todo lo que la app hace con la pregunta y con la key está en un archivo que se lee en GitHub o en DevTools. No hay un backend mío en medio.
  • El arnés prueba el código que corre. index.html?test=1 ejecuta las mismas funciones que usa el asesor, no una copia.

Para recorrerlo: unas 1,150 líneas son estilos, 350 son las pantallas y el resto es JavaScript, partido en bloques con un rótulo /* ════ NOMBRE ════ */. Se busca el rótulo con Ctrl+F:

Rótulo Qué hace
STOPWORDS + SINÓNIMOS y MOTOR 2 — CORPUS UNIFICADO Y RETRIEVAL BM25 La búsqueda: BM25, variantes, erratas y cuándo decir «no está»
MOTOR 2 — RECONSTRUCCIÓN LAYOUT-AWARE y MOTOR 2 — DETECCIÓN DE FIGURAS Lee el PDF por columnas y recorta las láminas
MANUALES GUARDADOS EN EL DISPOSITIVO Guarda los manuales en IndexedDB para no reprocesarlos
MODO SIN MODELO — retrieval local, cero red El modo manual
CONSTRUCCIÓN DE CONTEXTO Qué fragmentos llegan al modelo
STREAMING y REINTENTOS Y RESPALDO Llamadas a Gemini y OpenAI, y el modelo de respaldo
EVIDENCIA VISUAL y VERIFICACIÓN CONTRA EL CONTEXTO Qué lámina se enseña y cuándo sale un aviso
ARNÉS DE MEDICIÓN — abrir con ?test=1 Las pruebas

Una cuarta parte del JavaScript son comentarios. Casi todos explican por qué una regla es como es, con la pregunta que la hizo necesaria.

Lo partiría en módulos si entra otra persona a trabajar en él. Se puede hacer sin bundler; el costo es que el service worker tendría que guardar varios archivos de la misma versión.

Cómo se mide

Abrir index.html?test=1 corre el arnés: preguntas de respuesta conocida sobre el manual interno —incluidas las que llegan por sinónimo, las escritas con errata, las de seguimiento y tres de ruido que deben quedar sin respuesta—, más las comprobaciones de verificación numérica, de láminas y de certeza. Reporta recall, cuánto ruido se atrapa y el margen del caso más ajustado sobre el corte relativo CTX_ALPHA, que es el número que hay que volver a mirar cada vez que se toca el vocabulario. Corre entero en el dispositivo, sin API y sin red, sobre las mismas funciones que usa el chat.

Corre solo en cada cambio. El CI (.github/workflows/arnes.yml) abre la app en un Chromium sin ventana con eval/arnes.mjs y marca en rojo el push o el pull request si alguna de las 232 filas falla. El repo sigue sin package.json: Playwright se instala solo en el CI. En local: node eval/arnes.mjs (con CANAL=chrome si no tienes el Chromium de Playwright).

Si hay manuales cargados corre además una segunda tanda contra ellos. No puede comprobar respuestas concretas —cada manual dice lo suyo—, así que mide lo que es igual en cualquier manual de piso: que las preguntas de siempre (pasillo, gancho, tallas, limpieza, surtido) encuentren algo, que el ruido no, que con una sección activa ni un fragmento ni una lámina salgan de otro manual, y que preguntar por una sección teniendo otra activa avise en vez de contestar. Esa última prueba se arma sola con el identificador que cada manual da de sí mismo, así que corre igual con manuales que el sistema no ha visto nunca. Medido sobre once manuales reales cargados a la vez —1.150 fragmentos, 706 figuras, 82 MB, 3 minutos de carga—: 33/33.

Lo que ese lote mide de verdad no es el acierto, es de dónde sale el dato. Con las once secciones cargadas y una activa, 88 preguntas de trabajo (ocho por sección) devolvieron cero fragmentos de otro manual y cero avisos indebidos; y nombrando otra sección, las once avisan con cero fragmentos y cero láminas, que es lo que hace imposible la cifra creíble y falsa. La contraprueba de por qué importa: «¿cómo circula el cliente en la sección?» la contestan diez secciones con siete cifras distintas, todas verdaderas en su manual.

La batería con respuesta conocida

El arnés de arriba comprueba que una pregunta encuentre algo. Eso no es lo mismo que encontrar la lámina que contesta, así que hay una segunda medida, hecha al revés: se vuelca el contenido de los once manuales, se escriben 88 preguntas en las palabras del asesor —un quinto de ellas sin compartir ni una palabra con la lámina que las responde— y cada una lleva anotada su página y su rótulo. Más 14 preguntas cuya respuesta no está en la sección activa, sin las cuales subir el recall es trivial y falso: basta con aflojar los cortes.

antes después
la página que contesta llega al contexto 94% (82/88) 100% (88/88)
escritas en palabras del asesor, no del manual 87% 100% (16/16)
se ofrece la lámina de esa página 79% 100% (71/71)
dos temas en una frase, los dos contestados 5/6 6/6
preguntas sobre el propio manual 0/12 12/12
negativas que no se entregan como evidencia sólida 10/14 13/14
fragmentos de otro manual 0 0

Los cuatro fallos de recall que quedaban eran una sola causa: el asesor escribe el verbo y el manual titula el sustantivo. «Doblo» no llegaba a DOBLADO, «cuelgo» no llegaba a COLGADO —la raíz cambia al conjugar—, «colorizo» no llegaba a COLORIZACIÓN. No era vocabulario: la lámina estaba ahí y se llamaba casi igual.

La batería vive en el repositorio del proyecto, no en la app: son 104 preguntas escritas a mano contra manuales concretos. Lo que sí queda dentro son las pruebas de las funciones que salieron de ella —morfología, preguntas de estado, palabra ausente, nombres sin respaldo, búsqueda en todas las secciones—, que corren en ?test=1 con y sin manuales.

Dos de esas preguntas no las escribí yo. Salieron del piso, con la API conectada y 101 MUEBLES abierto, y sus respuestas venían marcadas con 👎:

«¿cuál es la marca propia?» → «Haus es la marca propia de liverpool» «¿cuál es marca preferencial?» → «Haus es la marca preferencial»

Comprobado contra los manuales: «preferencial» no aparece en ninguno de los once, «Haus» solo en 365 BLANCOS, y el texto crudo del PDF de MUEBLES —38 páginas— no contiene ninguna de las dos palabras. Ninguna de las dos respuestas salió de un fragmento: salieron de que el modelo reconoce la cadena real de la que son estos manuales y completó con lo que sabe de ella. Es la última vía de invención que quedaba abierta, y la que ninguna comprobación tocaba: se verificaban cifras y páginas, y un nombre de marca pasaba limpio.

El motor de lectura de manuales

Un manual de campaña no es un documento de texto corrido: es una presentación. En una misma lámina conviven dos reglas distintas, una en cada columna, a la misma altura. Eso condiciona todo lo demás:

  • Reconstrucción por columnas. El texto se rearma en tres pasos —fragmentos → líneas, cortadas donde hay hueco horizontal → bloques, agrupados por cercanía— y solo al final se ordena, por bandas y de izquierda a derecha. Agrupar por coordenada vertical, que es lo que hace casi cualquier extractor, fusiona las dos columnas y produce una regla que no existe.
  • Cada fragmento sabe de dónde salió. Documento, página y sección viajan pegados al texto hasta el prompt, y la sección se asigna por posición —el título que está encima y en la misma columna—, no por orden de lectura. Citar mal la sección es peor que no citarla: por eso el título vigente se reinicia en cada página y, cuando la lámina no lleva ninguno en mayúsculas, se usa su propio nombre —el rótulo corto de la franja superior, "Rotación", "Perímetros de Básicos"—, que es lo que el asesor tiene delante en la hoja.
  • Figuras. Los planogramas de estos manuales son dibujos vectoriales, no fotos: extraer las imágenes incrustadas devuelve íconos de leyenda de 36×18 px y se deja justo lo que más se consulta. Así que se hace lo que hacen los parsers serios (Marker, MinerU): renderizar la página y recortar la región, encontrándola por geometría —dónde hay tinta que no es texto— como en PDFFigures 2.0, sin ningún modelo. Para las fotos, además, se le pregunta al PDF dónde coloca cada imagen. Cada figura hereda el texto que la rodea, así que ya es buscable sin ninguna IA.
  • Verificación de lo verificable. Con API key, cada cifra de la respuesta se comprueba contra los fragmentos que realmente se enviaron, cada página citada contra las que existen, y cada nombre propio contra los que el manual usa. Lo que no cuadra sale marcado. No detecta un razonamiento equivocado; detecta el dato traído de fuera del manual, que es el que llega al piso.
  • Los nombres se reconocen por lo que sabe el corpus, no por las mayúsculas. La respuesta que falló decía «Haus» al principio de la frase y «liverpool» en minúscula, así que fiarse de la capitalización de la respuesta no habría servido para ninguna de las dos. Lo que sí sirve es cómo escribe el manual: un nombre propio aparece en mayúscula a mitad de frase —«la marca Haus Kids», «el sistema de Mercaderías de Liverpool»— y no aparece nunca en minúscula. Las palabras corrientes fallan la segunda condición, así que no entran. Con eso, un nombre que está en otra sección —o en ninguna— y no en los fragmentos consultados sale marcado.
  • La lámina que acompaña sale de lo que la respuesta citó, no de lo que el buscador trajo: las páginas citadas se cruzan con los fragmentos enviados para saber de qué manual son, y si la respuesta no cita ninguna página no se muestra ninguna imagen. Los recortes repetidos se reconocen por una firma visual del propio recorte, no por su posición en la lámina.
  • Manda el manual del asesor. Con un PDF cargado, ese es el que se está ejecutando en su piso: su valor es el operativo, el conocimiento interno queda como referencia general y, si los dos hablan del mismo dato, la diferencia se menciona como nota — nunca como empate que deje al asesor eligiendo.
  • El contexto se recorta con números, no con intuición. Corte relativo al mejor fragmento de cada consulta, deduplicado de casi-idénticos dentro de un mismo documento, tope por página y nada de fragmentos cortados a la mitad: ~60% menos contexto, medido contra 51 preguntas de respuesta conocida en 17 manuales reales de dos plantillas distintas, sin perder ninguna. La regla de aceptación es de veto: una configuración que pierda un solo fragmento con la respuesta se descarta, ahorre lo que ahorre.
  • Descripción de figuras con IA, opcional y apagada. Un modelo con visión transcribe los rótulos de un plano una sola vez por figura, y el texto queda indexado. Sin key la app funciona igual, solo sin esa capa.

Dependencias y datos — lo que sí sale y lo que sí se guarda

Nada de esto es un problema, pero prefiero decirlo a que se descubra abriendo DevTools:

  • Carga tres librerías desde CDN (cdnjs): pdf.js para leer PDFs de temporada que el usuario arrastre, marked para el markdown y DOMPurify para sanitizar lo que se renderiza. Más las tipografías de Google Fonts.
  • Con API key, la pregunta sale del dispositivo hacia el proveedor que el usuario configuró. Es una llamada directa del navegador a su API, sin intermediarios míos.
  • Guarda en el navegador: la API key en sessionStorage (se borra al cerrar la pestaña, nunca en localStorage); el historial de chat, lo aprendido del piso y los accesos rápidos en localStorage.
  • Guarda los manuales procesados en IndexedDB, indexados por el hash del archivo, para no volver a procesar 28 láminas cada vez que se abre la página desde el celular. Incluye los recortes de las figuras. Vive en el dispositivo y no sale a la red, y hay un botón visible para borrarlo en la pestaña de manuales.
  • Si —y solo si— se pulsa "describir figuras", los recortes salen hacia el proveedor configurado. Es la única vez que una imagen del manual deja el dispositivo, y hace falta pedirlo a propósito.
  • Sin telemetría, sin analítica, sin cuentas. Nada se envía a ningún servidor mío, porque no hay servidor mío.

Límites conocidos

  • El retrieval no lematiza de verdad. Del lado de la pregunta prueba unas cuantas formas —plural en los dos sentidos ("pasillos" → "pasillo" y "maniquí" → "maniquíes"), género ("rebajado" → "rebajada") y, desde la batería, la derivación al sustantivo con la que estos manuales titulan: "doblo" → DOBLADO, "colorizo" → COLORIZACIÓN, y "cuelgo" → COLGADO cambiando la raíz, que es lo que hace el español al conjugar. Va en un solo sentido a propósito: al probar también el infinitivo, "cambio" alcanzaba "cambiar" y "¿cómo cambio la llanta del coche?" volvía a pasar por pregunta contestable. Sigue sin ser un lematizador y ningún salto de significado ocurre sin el diccionario de sinónimos, que se llena a mano y por lo tanto está incompleto. Lo que sí hay es dónde arreglarlo: las variantes se generan solo del lado de la pregunta, así que ampliarlas no obliga a reprocesar ningún manual ya guardado.
  • Los manuales son de una cadena que existe, y el modelo la reconoce. Es la vía de invención más difícil de tapar, porque lo que sale suena cierto y a veces lo es: «Haus es la marca propia de Liverpool» es verdad en el mundo y es falso en ese manual, que no dice ni una de las dos palabras. Hay tres defensas y ninguna es total: una regla del prompt que prohíbe usar lo que sepa de la cadena, un aviso en el contexto cuando una palabra de la pregunta no está en el manual activo, y la verificación de nombres, que es la única que no depende de que el modelo obedezca. Un dato de la cadena que sí esté escrito en otra sección del mismo manual sigue siendo indistinguible de uno recordado.
  • «Preferencial» no se puede separar de «preferencia» sin entender. El corrector de erratas las da por iguales —0.85 de parecido por trigramas— y «de preferencia, coloca…» sí está en el manual. Se le enseñó a no tratar como errata lo que comparte raíz, que es lo que distingue una falta de ortografía de una palabra distinta; pero una derivación con otro sentido sigue colándose. Ahí la red es la verificación de nombres, no la búsqueda.
  • Una palabra que el manual no menciona no se puede detectar por semántica, solo por ausencia. Con once secciones cargadas, "¿cómo acomodo las sábanas?" estando en ZAPATOS engancha con "acomodar" —que sí es de ese manual— y devuelve fragmentos correctos para una pregunta que es de otra sección. Lo que se comprueba es literal: si una palabra de la pregunta no tiene ningún camino hasta el manual activo (ni forma, ni raíz, ni sinónimo) y sí es tema de otro cargado, se dice cuál. Funciona con sustantivos concretos —sábanas, tequila, motos— y no distingue matices: dos manuales que usen las mismas palabras para cosas distintas siguen siendo indistinguibles para esto.
  • Las erratas se corrigen por parecido, no por diccionario. Cuando ni la palabra escrita ni ninguna de sus formas está en el índice, se busca la más parecida por trigramas y entra con el mismo descuento que un sinónimo. Alcanza para "entayado", "corvatas" o "maniquis"; no para una palabra mal partida ni para una que el manual sencillamente no usa.
  • La pregunta de seguimiento se resuelve por forma, no por comprensión. Solo se amplía con la pregunta anterior lo que está literalmente incompleto: empieza por "y…", "entonces…", o no llega a dos palabras propias. Lo demás se busca tal cual, y si no encuentra nada se reintenta con la anterior pegada aceptándolo solo si sale sólido. El umbral anterior —menos de cuatro palabras propias— ampliaba casi siempre, porque en español tras quitar "como", "se", "los", "en" quedan dos o tres: medido en el piso, "¿cómo se arman las mesas?" se buscó junto con la pregunta anterior y el asistente contestó la anterior, palabra por palabra. Sigue siendo una heurística: un seguimiento redactado entero no se detecta, y para eso está el aviso en la tira de fuentes.
  • Que dos manuales sean la misma plantilla es el riesgo de fondo, y no lo arregla la búsqueda. Medido entre 101 MUEBLES y 251 JUVENILES: 22 títulos idénticos y, bajo ALINEACIÓN, los dos dicen 80 cm. Pero "¿qué porcentaje es el cliente clásico?" vale 25.6% en uno y 0% en el otro. Con la sección equivocada activa la respuesta no sale vacía: sale un número creíble y falso, con su página y su lámina. Como el vocabulario de los dos manuales es el mismo, ningún ajuste del retrieval puede distinguirlos. Lo que sí distingue es cómo se llama la sección, y eso el asesor lo escribe: "los perímetros en juveniles". Así que el nombre de la sección se busca dentro de la pregunta, y si señala a otro manual cargado, el asistente no responde con datos — avisa y ofrece cambiar. Un término solo cuenta como identificador si de verdad señala a un manual: medido sobre once, "juveniles" sí (11 de 11 fragmentos son suyos) y "muebles" no (16 de 85, con dulcería pisándole con 15, porque todas las secciones tienen muebles de exhibición).
  • Hay secciones que solo se pueden nombrar por su número. Cuando el nombre de una sección es una palabra que todos los manuales usan, no queda identificador: 101 MUEBLES y 271 CASUAL solo se alcanzan escribiendo "101" o "271" —y "casual" es peor que ambiguo, tiene 26 fragmentos en el manual de zapatos contra 15 en el suyo—. Para las que se quedarían sin ninguna palabra se baja el listón a dominancia clara —más del doble que el segundo manual y al menos el 40%—, que es lo que recupera "blancos" (10 de 15, con el segundo en 4). Ese rescate no se aplica cuando ya hay una palabra: "512 MUJER CLÁSICA" se identifica por "clásica", y sumarle "mujer" bloquearía preguntas legítimas en vestidos de fiesta.
  • El rótulo de la sección se adivina, y a veces se adivinaba mal. Sale del código de sección en los títulos, del check list, o del nombre del archivo cuando trae el código delante. Medido sobre once: el manual de zapatos no tiene ninguna de las dos marcas internas y salía llamándose "ENTRADA PEATONAL" —el título de una lámina de la página 2—, y el de vinos salía como "388 DIVERSOS" porque así lo rotula su propio check list. El nombre del archivo se prefiere solo si no comparte ninguna palabra con el rótulo interno, y la comparación es por prefijo porque al descargar se pierde el acento: "391 DULCER A" sigue siendo DULCERÍA y conserva su nombre interno. Un manual sin código en ninguna de las tres vías sigue cayendo al nombre de archivo limpio.
  • Las palabras que el asesor usa y no son el nombre de nadie —"sábanas", "sneakers", "tequila", "comedor"— no identifican sección, y no hace falta que lo hagan: al no encontrar nada en la sección activa entra la vía de evidencia y, medido, las seis dan con su manual con cero fragmentos y cero láminas. El límite está en la otra dirección: si la sección activa sí tiene algo parecido que decir, responde con lo suyo y no avisa.
  • "Todos los manuales" sirve para comparar, no para trabajar. Sin sección elegida la misma pregunta tiene varias respuestas verdaderas: medido, "¿qué porcentaje es el cliente práctico?" devuelve 44%, 38.5%, 39.3%, 35.3% y 43.8% — una por sección. Elegir una es acertar en una y fallar en cuatro. Ahora el contexto avisa de cuántas secciones lleva y pide el dato por sección, y no se enseña ninguna lámina salvo que la respuesta cite la página: una imagen que dice "de aquí sale el dato" cuando hay cinco datos distintos es una afirmación falsa. Aun así, en el piso lo correcto es elegir sección.
  • El filtro de fuera de tema se agujerea al crecer, y por eso el vocabulario es el de la sección activa. Para decidir si una pregunta es del dominio se mira si alguna palabra está en el manual. Con once cargados esa unión llega a 2.952 palabras y ya casi todo está en algún manual: medido, "dame la receta del pastel de chocolate" pasaba a contar como pregunta de trabajo por chocolate, de dulcería, y "¿cómo va el clima mañana?" por clima. Con la sección activa el vocabulario vuelve a 972 palabras y las dos se atrapan otra vez. Sin sección elegida el agujero sigue abierto, que es una razón más para elegirla.
  • El modo manual puede devolver una coincidencia floja. Si una lámina contiene por casualidad una palabra de la pregunta, la muestra. Probé dos filtros para cortarlo —por rareza del término y por puntuación mínima— y medí los dos sobre los siete manuales reales: ninguno funciona, porque el ruido y las preguntas legítimas se solapan. Lo que el modo garantiza no es rechazar lo que no sabe, sino no fingir que lo sabe: entrega el fragmento con su página y dice que nadie lo interpretó. Lo que sí se cerró es el ruido por coincidencia parcial: el acierto se cuenta por palabra entera, así que «¿cómo cambio la llanta del coche?» ya no acierta dentro de «cambiar cada 3-4 semanas». Las formas legítimas las sigue generando el lado de la pregunta.
  • Un PDF escaneado no se puede leer. Si la lámina es una imagen sin capa de texto, el sistema lo dice y no lo carga, en vez de fingir que lo entendió. Haría falta OCR.
  • La detección de figuras está calibrada sobre manuales tipo presentación. El corte entre "figura" y "panel de texto" salió de medir un manual real: las figuras quedan por debajo de 0.11 de cobertura de texto y los paneles por encima de 0.27. Un documento con otra maquetación puede caer en el hueco, y entonces sobran o faltan recortes.
  • La verificación de cifras no distingue entre inventar y razonar. Comprueba que el número —con su unidad, porque «80 cm» y «Cruce 80/20» no son el mismo dato aunque compartan el 80— esté en el contexto recuperado; si el modelo suma dos cantidades correctas, el resultado saldrá marcado aunque esté bien. Prefiero ese falso positivo al silencio. Y el aviso va encima de la respuesta, no debajo: colgado abajo, el asesor ya había leído —y en el piso, ejecutado— el dato que el aviso venía a poner en duda.
  • El listón de "esto sí es una respuesta" depende de la escala, y es un número medido, no deducido. Con las 10 secciones largas del manual interno, acertar una palabra dentro de «BÁSICOS DE DISPLAY» apunta de verdad a la regla; con 572 fragmentos de 185 caracteres de cinco manuales reales, la misma palabra suelta es casualidad —«tienda» aparece en «TIENDA FLAGSHIP» y colaba «¿a qué hora abre la tienda?» con 8412 caracteres de contexto—. Por eso la exigencia sube con el tamaño del corpus. Los dos regímenes medidos están lejos (10 fragmentos contra 88–572), así que el corte no está afinado al borde; pero es un número que habrá que volver a mirar con manuales de otra maquetación.
  • La puntuación general premia [MANDATORY] aunque no haya coincidencia de palabras. Sirve cuando el resultado lo va a leer un modelo, que sabe descartar lo que no viene al caso; mentiría en el modo manual. Por eso ahí se exige al menos un acierto real de término antes de mostrar cualquier sección.
  • La lámina que se muestra puede ser la vecina. Si en la página citada ninguna figura se puede identificar por sección ni por las palabras de la respuesta, se muestra una —no tres, que se leen como tres pruebas del mismo dato— y el rótulo dice que es de la página citada, no que sea la lámina que sostiene el dato. El pie lleva siempre manual, página y sección para que se vea de dónde salió.
  • Si el modelo no cita página, la lámina sale igual, con otro rótulo. Antes eso dejaba al asesor sin ninguna imagen, que era la falla más visible: basta con que el modelo se salte el formato para perder la evidencia. Ahora se cae al fragmento mejor puntuado de los que se consultaron y el rótulo lo dice con esas palabras — no promete una cita que no existe. Lo que nunca sale es una lámina bajo una respuesta que admite el hueco.
  • El recorte de contexto se midió sobre 17 manuales de dos plantillas distintas —51 preguntas de respuesta conocida, ninguna perdida—, pero el margen del caso más ajustado bajó de 51% a 42% al ampliar el diccionario: cada sinónimo nuevo sube la puntuación del mejor fragmento y, con ella, el listón del corte relativo. Sigue habiendo holgura sobre α=0.25, pero es un número que hay que volver a medir cada vez que se toca el vocabulario, no una constante. El arnés ya vive en el repo (index.html?test=1) precisamente por esto: al ampliar variantes y erratas el margen del caso más ajustado se movió del 35% al 75%, y eso solo se ve midiendo.
  • El nombre de la lámina se deduce de la maquetación. Se toma la línea corta y sin puntuación de la franja superior de la página. Acierta en los 17 manuales, pero es una regla geométrica: en una lámina cuyo rótulo esté partido en dos líneas se queda con la etiqueta del panel ("Montaje") en vez del título completo ("Mercadeo de Cestos en Perímetro"). Es menos preciso, no falso.
  • El conocimiento es sintético, así que las respuestas son coherentes pero no son el estándar de nadie. Sirve para ver la mecánica, no para montar una tienda. Con un PDF real cargado deja de entrar del todo: el manual del asesor es la única fuente. Hubo un botón que lo consultaba a propósito como «referencia general», y llevaba roto desde que se escribió —preguntaba por un campo que la función no devuelve, así que siempre contestaba que no lo cubría sin haber buscado—. Arreglarlo habría sido peor que dejarlo: devolver reglas de un manual de demostración rotuladas como política de la cadena es exactamente la clase de dato plausible y falso que este proyecto persigue. Ahora ese botón busca donde sí hay verdad: en los otros manuales del asesor, con cada dato citado a su sección y su página.

Lo que encontré al prepararlo para publicar

Defectos que el uso normal no muestra. Los dejo escritos porque enseñar dónde falla un sistema dice más de él que la lista de lo que hace:

  1. El hash de integridad de DOMPurify estaba mal. El navegador descargaba el archivo, la verificación SRI fallaba y el script no se ejecutaba nunca. El código tenía un fallback (escapeHtml) que funcionaba tan bien que nadie notó nada: llevaba meses renderizando todas las respuestas como texto plano escapado, y la protección que decía tener no estaba cargada. Un fallback silencioso convierte un bug en una feature invisible.

  2. El retrieval no sabía decir "no sé". La función que puntúa las secciones suma puntos por ser [MANDATORY] o por documentar un conflicto, aunque no coincida ni una palabra de la pregunta. Resultado: preguntar por una receta de cocina devolvía la sección de entallado, con toda seriedad. Sirve cuando el resultado lo lee un modelo, que descarta lo que no viene al caso; en el modo manual era mentir. Ahora se exige al menos un acierto real de término.

  3. El bloque de fuentes se desbordaba en celular. Justo en el único dispositivo donde esto se usa.

  4. El asistente nunca había leído un manual cargado. Este es el grande, y tardé meses en verlo porque estaba buscándolo en el sitio equivocado. Cuando la respuesta sobre un PDF salía mal, yo culpaba al prompt o al modelo. El problema estaba mucho antes: la extracción agrupaba el texto solo por coordenada vertical, y en una lámina con dos reglas en paralelo —ALINEACIÓN a la izquierda, LIMPIEZA a la derecha, a la misma altura— las fusionaba en la misma línea. El "¿Qué es?" de una quedaba pegado al de la otra. Después, el troceado cortaba ese texto ya revuelto cada 700 caracteres. El modelo nunca recibió el manual; recibió el manual licuado, y ningún prompt arregla eso. Cuando un sistema con IA falla, el instinto es tocar el prompt, porque es la pieza que se ve.

  5. Sin API key, el PDF recién cargado se ignoraba por completo. La búsqueda del modo manual solo recorría el conocimiento embebido. El usuario veía su manual en la lista, preguntaba, y recibía respuestas que no salían de él.

  6. El filtro de honestidad tiraba justo las respuestas buenas. Salió probando con siete manuales de secciones distintas, y es el reverso del hallazgo 2. Para poder decir "no sé", el modo manual exigía que alguna palabra de la pregunta apareciera literalmente en el fragmento. Pero el manual de Caballero contesta "¿cuánto dejo de pasillo?" con una lámina titulada ALINEACIÓN que dice "dejando 80 cm" y nunca escribe la palabra pasillo. BM25 la encontraba igual, por el diccionario de sinónimos, y la ponía primera — y el filtro la descartaba por no tener coincidencias literales. El puente estaba construido y el filtro no dejaba cruzarlo. Se sumaba que cm, en un manual que es medidas de punta a punta, no era ni siquiera una palabra buscable: el tokenizador descartaba todo lo de dos letras. Las tres formas de preguntarlo fallaban; ahora las tres caen en la lámina correcta.

  7. Dos filtros que parecían buenos y no lo eran. Para cortar las coincidencias incidentales probé filtrar por rareza del término (idf). Falla de raíz: "cambio" aparece en un solo fragmento y puntúa 4.17, por encima de "sensor" (3.66) — raro no es lo mismo que relevante. El segundo intento, una puntuación mínima, parecía perfecto en un manual (el ruido en 3.3–4.4, lo legítimo desde 6.5) y se derrumbó al medirlo en los siete: "¿a qué hora abre la tienda?" llega a 7.5 porque tienda es palabra central del manual. No hay corte que separe. Los dos se quedaron fuera. Calibrar un umbral con un solo documento es como probar el código con un solo caso: sale bien y no significa nada.

  8. La respuesta no llegaba: se cortaba justo antes del dato. Durante meses interpreté esto como "la IA contesta mal". No contestaba en absoluto. Los modelos con razonamiento interno —gemini-3.5-flash entre ellos— descuentan de maxOutputTokens lo que piensan por dentro, y con seis etapas de razonamiento visible el presupuesto se agotaba pensando. En pantalla quedaba el razonamiento truncado a media frase, y el fallback del parser lo presentaba como si fuera la contestación: parecía que había respondido. Subir el tope no bastó (con 8000 se seguía cortando); el arreglo es apagar el pensamiento interno del proveedor, que aquí sobra — este asistente ya razona en seis etapas que se pueden leer, que es exactamente lo contrario de un razonamiento que no se puede auditar. Ahora, además, un razonamiento sin respuesta final se anuncia como lo que es y no se disfraza de respuesta.

  9. El verificador de cifras marcaba como inventado todo número decimal. La defensa estrella contra la alucinación tenía un bug de dos líneas: n.replace('.','[.,]').replace(',','[.,]') — el primer replace mete una coma dentro de la clase de caracteres y el segundo la vuelve a sustituir, produciendo 20[.[.,]]8, un patrón que no coincide con nada. Resultado: preguntar por el 20.8% de participación devolvía el dato correcto con un sello de "no pude verificarlo", y estos manuales son decimales por todas partes (20.8%, 38.5%, 11.5%). Un aviso que desconfía de lo correcto gasta la credibilidad que necesita para cuando de verdad haya una invención.

  10. La imagen decía que el dato salía de ahí, y no salía de ningún lado. A la pregunta por la temperatura de la sección el modelo contestó bien —"el manual no lo especifica"— y debajo aparecieron dos láminas igual, además idénticas entre sí. La tira de evidencia se armaba con los ~20 fragmentos que entraron al contexto, no con lo que la respuesta acabó citando, y el descarte de plantilla comparaba la posición del recorte, no su contenido. Un asesor cree la foto antes que el texto: una lámina que no sostiene lo que se lee es peor que ninguna. Ahora la tira se arma con las páginas que la respuesta citó —desambiguadas contra los fragmentos, porque con dos manuales cargados "pág. 11" es ambiguo—, sin cita no se muestra nada, y si de la página se puede identificar la lámina (su sección es la del fragmento citado) se enseña esa sola en vez de la página entera.

  11. Recortar el contexto sin perder la respuesta: medido, no supuesto. Se enviaban ~20 fragmentos por pregunta, y ahí es donde se va la cuota. Se midieron cinco filtros sobre 24 preguntas de respuesta conocida en los siete manuales —seis de ellas formuladas sin las palabras de la lámina, que son las únicas que ponen a prueba un corte por puntuación—, con una regla de veto: cualquier configuración que pierda un solo fragmento con la respuesta queda descartada aunque ahorre mucho. Quedó un corte relativo al mejor fragmento de cada consulta (el absoluto ya había fallado, hallazgo 7), más deduplicado de casi-idénticos, tope por página y prohibición de enviar fragmentos truncados: 60% menos contexto sin perder ninguna respuesta. El α agresivo ahorraba 72% y también pasaba, pero el caso más difícil quedaba a la mitad de margen; no vale la pena por una pregunta que no esté en la muestra.

  12. Un deduplicado sensato borró el dato. El filtro de casi-duplicados comparaba solo el cuerpo del fragmento. Con dos manuales cargados, la lámina «PRÁCTICO (20.8%)» de Blancos y la «PRÁCTICO» de Caballero tienen el mismo párrafo debajo —comparten plantilla— y el dato distintivo vive en el título. El filtro tiraba la de Blancos por parecida, y la respuesta correcta salía con el sello de "no pude verificar 20.8". Se arregló metiendo el título en la firma y no comparando nunca entre documentos distintos: que dos manuales digan lo mismo no vuelve prescindible al del asesor.

  13. Un número es un dato para buscar y una coincidencia para reconocer. Al elegir qué lámina acompaña a la respuesta, «(pág. 3) … el 20.8%» compartía el «8» y el «3» con una lámina titulada «VANGUARDISTA (8.3%)», que no tiene nada que ver, y la ponía como evidencia. Las cifras sueltas quedan fuera de ese cotejo — no del buscador, donde un "40%" sí es información.

  14. La sección se heredaba de la página anterior. El título vigente arrancaba con el documento, no con la página, así que una lámina sin títulos en mayúsculas se quedaba con el de la anterior: la regla del producto descontinuado se citaba como «pág. 9 · ESQUINEROS», que es una sección de la pág. 8. Y como el título también se indexa, no solo se citaba mal: se buscaba mal. Solo se vio al probar manuales de otra plantilla, donde las láminas llevan su nombre en minúsculas y el detector de títulos no lo reconocía.

  15. Cada lámina ya traía su nombre; nadie lo estaba leyendo. "Rotación", "Planograma", "Perímetros de Básicos" están arriba a la izquierda de cada página, pero en minúsculas, y el detector solo aceptaba mayúsculas. Acababan de primera línea del cuerpo. Reconocerlos dio sección a 17/17 manuales y dejó en cero las figuras sin sección — el pie de foto pasó de "pág. 8" a "pág. 8 · Montaje".

  16. Dos de los cuatro fallos que encontré eran de mis pruebas, no del producto. Un regex pedía "no desarmar el set" y el manual parte la frase en dos líneas; otro daba por hecho que Manual Blancos documenta la altura del sensor, y no la documenta. Un arnés que falla por su cuenta gasta el tiempo en el sitio equivocado y, peor, esconde los fallos de verdad: los dos reales —"espacio de paso" y "lo rebajado"— estaban en la misma lista.

  17. El proveedor de fábrica llevaba casi un año muerto, y la app culpaba a la señal. GitHub Models era la opción por defecto y la app llamaba a models.inference.ai.azure.com, que dejó de existir en octubre de 2025; GitHub retiró el servicio entero en julio de 2026. La petición fallaba antes de salir y el error se leía como "Revisa tu conexión a internet". Se quitó, y Gemini quedó por defecto. El que quedaba, gemini-3.5-flash, se satura del lado de Google: seis reintentos (de 1 a 30 s) sumaban casi minuto y medio antes del error. Ahora son tres, y a la segunda saturación contesta gemini-3.5-flash-lite y la tarjeta lo dice; con todo saturado, el error sale en 8 s. El respaldo obvio era gemini-2.5-flash, pero Google dejó los 2.5 solo a cuentas que ya los usaban: con una key nueva —la de quien prueba el demo— el respaldo también habría fallado. Y lo de apagar el pensamiento interno (hallazgo 8) no se pide igual en los 3.x: usan thinkingLevel, no thinkingBudget, y lo más bajo es MINIMAL.

Relación con Veristack

Es la otra mitad del mismo problema. Veristack verifica con una foto que la exhibición esté bien montada; este asistente ayuda a montarla bien desde el principio. Los dos parten del mismo insumo —el estándar operativo escrito— y comparten el mismo principio: cuando no hay evidencia suficiente, el sistema lo declara en vez de inventarlo.

Historia

Nació de una generación anterior del proyecto (2026), construida sobre conocimiento propietario de un cliente. Este repositorio se creó con historia nueva y limpia, deliberadamente: esa historia no debe existir en público. Lo que se publica aquí es la interfaz y la mecánica, con conocimiento de demostración.

Licencia

MIT.

About

IA para el piso de venta: responde dudas de montaje desde el celular, cita la página y la lámina exacta del manual, declara su certeza y dice «no sé» cuando no hay evidencia. 1 archivo HTML, búsqueda local (BM25), sin backend.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages