Synaplan
開源 AI 平台,供企業使用:自行託管的 AI 代理、符合 GDPR 的聊天機器人小工具、RAG 知識庫、WhatsApp 與 Outlook AI、MCP、多模型路由。Docker、Kubernetes 或託管於 Germ
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Responde con la página exacta del manual, declara su certeza y dice «no sé» cuando no hay evidencia. Búsqueda local (BM25), modo con API opcional, sin backend. Código visible; uso comercial solo con licencia.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗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ó: de un problema muy concreto: nadie abre un manual de 40 páginas frente al mueble, así que las dudas se resuelven preguntando o adivinando. Quise ver si una IA podía contestar con la página exacta del manual, declarar qué tan segura está y decir «no sé» cuando el manual no lo dice. Es un proyecto personal; más sobre mí 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, medida ni dato operativo de un cliente real vive en este repositorio.
▶ Abrir el demo · sin API key funciona en modo manual; con tu propia key (Google AI Studio u OpenAI) activa el modo razonado.
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.
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) y de los dos arreglos de la búsqueda de abajo. Falta volver a medirlos con esos manuales.
Los números de arriba son de manuales reales que no están en el repo. Para que cualquiera los
pueda repetir, el modo con API también se midió con los 4 manuales ficticios y las 99
preguntas del gate (eval/corpus-publico/):
node eval/modo-ia.mjs --local eval/corpus-publico/manuales.json \
--preguntas eval/corpus-publico/bateria.json --clave-en-pagina \
--cdp http://127.0.0.1:9333 --pagina 9611 --motor clasico --etiqueta api-clasico
Con --cdp, la medición pregunta en una pestaña de Chrome ya abierta (con
--remote-debugging-port) donde la persona pegó su key en Ajustes. La key se queda en el
sessionStorage de esa pestaña: el script no la lee ni la escribe en ningún lado.
| 3.5 Flash-Lite, razonado | Motor clásico | Agente lector |
|---|---|---|
| Trae el dato y cita su página | 78/84 (92.9 %) | 67/73 (cuota agotada en la 74) |
| Dice «no está» en las que no están | 14/15 | sin medir todavía |
| Con el dato en el contexto, lo dio bien | 78/78 | — |
| Primer texto, mediana / 95 % | 1.5 s / 1.9 s | 0.9 s / 4.1 s |
Lo que más se pregunta en el piso son los básicos: qué marcas van en qué mundo, cómo se clasifica la sección y cuál es la fórmula de exhibición. Probando la app salían dos fallas: el modelo no llegaba a las marcas y, en clasificación, explicaba cómo se clasificaba sin decir cuáles eran las categorías. Se armó una batería de 61 preguntas de básicos, con un manual nuevo de Básicos de Display y los 14 manuales reales. Son 42 de desarrollo y 19 de confirmación, con hash y corridas una sola vez. Todo queda fuera del repo.
| 3.5 Flash-Lite, razonado | antes (ap-v1.21) | ahora |
|---|---|---|
| Desarrollo (42): dato con su página | 26/42 (61.9 %) | 40/42 (95.2 %) |
| — «¿en qué mundo va X?» | 3/8 | 8/8 |
| — marcas de un mundo | 6/12 | 11/12 |
| Confirmación (19), una sola corrida | — | 17/19 (89.5 %) |
| Modo manual, desarrollo (sin API) | 21/42 | 35/42 |
Tres arreglos lo explican:
tablasDeRotulos en
src/motor/layout.js). Antes el rótulo del mundo le caía a la lista de la fila de abajo, y el
contexto ponía en PREMIUM una marca de otro mundo.src/ia/laminas.js). Si en el contexto llega
una página con casi todo dibujado, como «Clasificación de Departamentos», el modelo recibe
también su imagen. Lo mismo pasa con la lámina de MARCAS en una pregunta de marcas, porque
ahí muchas marcas son logos. Es una imagen por pregunta y sube la espera ~0.5 s.Las tres que fallaban se arreglaron en la lectura y la búsqueda (ap-v1.24), sin tocar el prompt:
tablaDeCeldas). La clasificación de una sección suele ser una
rejilla —«Botones | Material | Tamaño»— sin rótulos en mayúsculas. Se reconoce por la forma:
cuatro o más filas de celdas cortas y alineadas, rótulos que no se repiten y valores que sí.Modo manual: desarrollo 35 → 36/42, confirmación 16 → 17/19; dato en contexto, desarrollo 38 → 39/42. Golden de 667: arregla 1, rompe 0.
Con la API, otra vez, después de esos arreglos (ap-v1.25, 11-oct, noche). Mismos 15 manuales, misma batería de desarrollo, tres corridas:
| 3.5 Flash-Lite, razonado | ap-v1.25.0 | ap-v1.25.2 |
|---|---|---|
| Dato con su página (40) | 38/40 + 2 esperas cortadas (al repetirlas, bien) | 39/40 |
| Página correcta | 38/40 | 40/40 |
| «No está» bien dicho | 2/2 | 2/2 |
| Respuestas correctas con un aviso falso | 7 | 0 |
| Primer texto, mediana / 95 % | 2.1 s / 21.9 s | 1.9 s / 2.8 s |
Desde el 26-sep, en Ajustes → Modo de respuesta hay dos:
Con el manual demo (eval/preguntas-demo.json, 36 preguntas), el 26-sep, con
node eval/modo-ia.mjs --modo razonado|rapido:
| Razonado | Rápido | |
|---|---|---|
| Trae el dato y cita su página | 24/24 | 23/24 |
| Dice «no está» en las que no están y en las trampas | 12/12 | 12/12 |
| Avisos de «no pude verificar» | 0 | 0 |
| Primer texto en pantalla, mediana / 95 % | 2.0 s / 6.7 s | 0.9 s / 2.2 s |
La que se le escapa al rápido es «la barra se ve muy llena»: la búsqueda trae SATURACIÓN en primer lugar, pero Flash-Lite a veces contesta con la lámina de alineación. En la corrida anterior del rápido salió bien (24/24), y en el razonado ya había fallado por la mañana. Es la pregunta más inestable del demo.
El rápido no es el de fábrica todavía: el #26, que también quitaba las etapas, empataba con el demo y perdió aciertos justo con los manuales reales (51/60 contra 55). Hasta medirlo con esos 30, queda como opción.
El modelo de fábrica ahora es 3.5 Flash-Lite. Era el que contestaba de verdad: en todas las mediciones 3.5 Flash salió saturado y respondió su respaldo, que es Flash-Lite. Ponerlo primero quita los reintentos de la primera pregunta. 3.8 Flash sigue en la lista, pero en el demo no contestó mejor: 23/24, ~6 s de mediana, se saturó a media prueba y dos de sus respuestas llegaron cortadas (el razonamiento sin la contestación).
Tres arreglos salieron de estas mediciones:
Lee tu manual → lo parte en láminas → preguntas como hablas → responde con la página y la lámina. Grabado en la app publicada, a doble resolución, con el manual de ejemplo: la pregunta, la lámina que gana, el texto y la medición salen del motor real. Probarlo tú.
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".
Son dos herramientas en la misma app:
| 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 o, en modo rápido, con un solo paso de lectura |
| 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, 6 páginas por petición. 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.
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:
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.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 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:
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 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.
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 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.
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:
Con candados, porque aprender mal empeora lo que ya funcionaba:
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.
La pestaña Revisar apunta la cámara a una tringla, un anaquel o un focal y en un segundo
regresa la foto marcada, con un veredicto por básico y su porqué. Una foto de área se revisa
por zonas: con «+ Zona» se encierra la mesa, la tringla y el focal de la misma foto, y
cada zona trae sus básicos y un resultado general. Todo corre en el teléfono:
sin red, sin costo, sin subir la foto a nadie y sin IA que juzgue la foto. El código mide
y cada resultado lleva sus números (fuente: CÓDIGO). Si la foto no da para calificar
(oscura, movida o sin suficientes elementos), sale NO_CALIFICA: nunca se inventa un
CUMPLE. El diseño y los criterios de éxito, fijados antes de medir, están en el
ADR 0007.
| Básico | Qué mide el código | Qué marca en la foto |
|---|---|---|
| Colorización | Una franja a la altura del pecho, por columna en CIELAB; la corta en tramos de color y los clasifica en cálido, frío o neutro (los cafés, beiges y el azul marino son neutros; la mezclilla es fría). Antes, un balance de blancos quita el tinte de la luz de tienda. Cada grupo va en un solo bloque. | Una barra por tramo y el tramo que rompe el orden («frío entre cálidos, tramo 4 de 12») |
| Surtido | Celdas con bordes de Sobel contra el color del fondo del mueble; un hueco es vacío de arriba abajo, no el aire de encima de una pila, y deja ver el mueble (lo casi negro es sombra o producto negro, no hueco). | Los huecos rayados y el % vacío medido |
| Triangulación | Los puntos altos de cada elemento: una sola cima y desnivel suficiente. Vale el simétrico y el asimétrico (la cima a un lado, bajando por bases y bolsas). Toque inteligente: tocas cada elemento donde sea y su silueta (MediaPipe Magic Touch) da su punto más alto; la silueta se ve, y «Punto exacto» vuelve al toque a mano. Un detector (EfficientDet-Lite0) sugiere los puntos: busca en el recorte de la zona, quita el fondo y las bases, afina cada uno con la silueta y no pisa lo que ya tocaste. Medido con 5 focales de tienda: encuentra 11 de 17 cimas y 11 de 12 sugerencias caen en un elemento (antes, 8/17 y 8/11). Con «Punto exacto» sale una lupa, y cada punto se arrastra. | El triángulo, las bandas de cada altura y, si dos empatan arriba, «sube este» |
| Alturas y niveles | Con los mismos puntos: cuántas alturas distintas hay (alto, medio y bajo). | La cifra de alturas |
| Evidencia de la foto | No adivina si es IA: prueba de dónde salió. Tomada en la app (con huella SHA-256), de galería (EXIF: cámara, fecha, editor) o con su credencial de contenido (C2PA), que se lee y valida con el SDK oficial (c2pa-web) contra la lista de confianza oficial de C2PA (CC-BY-4.0, copia del 13-ago-2026): manifiesto activo, cadena de padres y firma. Una original firmada por una cámara de la lista (el Pixel 10 firma todas) y tomada en las últimas 24 h sale CUMPLE. Va aparte del veredicto del montaje: una foto reenviada por WhatsApp sale «sin datos». Cuando la app no puede probar el origen, un botón opcional manda la foto original a SynthID (Gemini en el teléfono, el portal en la computadora) para buscar la marca de agua de la IA de Google, y la persona anota lo que vio. Ningún detector de píxeles juzga la foto. | solo si la foto declara que con IA; retocada con IA o alterada después de firmar, |
Lo que una foto no puede probar (planchado, limpieza, alineación, enganchado, sensores, entallado, el pasillo de 90 cm y, en el focal, composición, equilibrio y simetría) va en una lista para marcar a mano. La foto marcada y el resumen se comparten por WhatsApp con la Web Share API.
Medido hasta hoy, con honestidad:
node eval/revision.mjs --sinteticas, tres niveles de suciedad):
triangulación 72/72, niveles 36/36, surtido 54/56 y colorización 52/56. Casi todos los
fallos están en el nivel «duro», donde un naranja en sombra fuerte es café para la cámara.
Prueban el método, no el criterio del ADR.node eval/revision.mjs --fotos <carpeta>, fuera del repo):
REGLA_COLOR.Sin cámara se puede probar con Ejemplo (una tringla, un anaquel y un focal dibujados por código, con y sin defecto) y con tres muestras de origen armadas en la app (foto de cámara, reenviada por WhatsApp y hecha con IA).
[MANDATORY], los
conflictos documentados y, cuando la pregunta pide una cantidad, para los fragmentos que
traen cifras.Hasta octubre de 2026 todo vivía en un index.html de 12,000 líneas, y fue a propósito: abre
sin señal, se publica sin compilar y se puede revisar entero. Cuando el motor tuvo que usarse
fuera del navegador (medir en Node, pruebas de propiedades, un servidor MCP), lo partí en
módulos ES nativos, todavía sin bundler (ADR 0005).
Lo de antes se mantiene:
sw.js) guarda la página, cada archivo de src/,
las tres librerías del CDN y las fuentes. El código de src/ va primero a la red, igual
que la página, para que el celular nunca junte un HTML nuevo con un JavaScript viejo. Si un
archivo de src/ falta en la lista, el arnés falla.index.html?test=1 ejecuta las mismas funciones
que usa el asesor.| Archivo | Qué es |
|---|---|
index.html | Estilos y pantallas |
src/main.js | Arranque: carga los módulos y los publica para el resto de la app |
src/motor/ | El motor, sin DOM: se importa desde Node. texto.js (stopwords, sinónimos, tokenización), indice.js (BM25), erratas.js (erratas, fonética y vocabulario de cada manual), layout.js (de pdf.js a bloques con título), fragmentos.js, secciones.js (de qué sección es cada manual y cuál nombra la pregunta), solidez.js (cuándo una coincidencia cuenta), aprendido.js (lo que enseñó el piso), busqueda.js (retrieve y el empaquetado del contexto), puerta.js (palabras ausentes, la cifra que falta y el contrato de decisión: respaldada, parcial, aclarar o sin_evidencia, con su evidencia y sus razones), ruta.js (contra qué sección se responde), conversacion.js (si la pregunta es del tema, el seguimiento y la ruta, con el historial como parámetro) y respuesta.js (las tarjetas del modo manual y el contexto del modelo, cada uno con su contrato) |
src/estado.js | El estado del corpus en un solo objeto (manuales, fragmentos, índice, lo que entró al contexto), el registro de cachés y conEstado, con el que el arnés monta un corpus de prueba y siempre vuelve al de antes |
src/seguridad/ | Saneado del HTML y defensa contra instrucciones escondidas (ADR 0004) |
src/revision/ | La revisión con foto (ADR 0007). Piezas puras sobre {width,height,data} que corren igual en Node: procedencia.js (EXIF, XMP/IPTC, C2PA y huella), color.js, surtido.js, triangulo.js, veredicto.js y demo.js (imágenes sintéticas). Solo ui.js (cámara, marcas y compartir) y detector.js (MediaPipe, carga perezosa) tocan el navegador |
src/app.js | Lo que falta por partir, en bloques con rótulo /* ════ NOMBRE ════ */ |
El corte va por capas: estado, índice, búsqueda, puerta de evidencia, verificación, IA y UI.
Cada paso tiene que dejar idéntico, campo por campo, lo que el motor hace con cientos de
preguntas sobre manuales reales (golden master, lab/identico.mjs). Los tipos se revisan
con tsc --checkJs sin compilar nada (npm run tipos), y las propiedades del motor se
prueban con fast-check (lab/motor.test.mjs).
Una cuarta parte del JavaScript son comentarios. Casi todos explican por qué una regla es como es, con la pregunta que la hizo necesaria.
El ícono es el monograma AP (icons/icono.svg): la A es la triangulación del display, con su punto focal arriba, y la P es una lámina con la esquina doblada. Los PNG del manifest (192 y 512 redondos, 512 maskable, 180 de Apple), icons/og.jpg y docs/banner.png salen del SVG y de capturas de la app real con el ejemplo sintético:
PLAYWRIGHT_CORE=<ruta a playwright-core/index.mjs> CANAL=chrome node design/iconos.mjs
El color vive en el fondo: una aurora de tres orbes (rojo de la marca, naranja y azul) detrás del vidrio, que se mueve despacio y se corre según la pestaña. Con «reducir movimiento» se queda quieta y con «reducir transparencia» desaparece. El rojo como estado sigue marcando solo la pestaña activa.
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 320 filas falla. La app no se compila: el package.json es solo para las
herramientas de desarrollo, y en el CI Playwright se instala solo. En local: npm run arnes
(con CANAL=chrome si no tienes el Chromium de Playwright).
El mismo CI corre eval/despliegue-sw.mjs: un servidor que se porta como GitHub Pages
(max-age=600) publica una versión nueva a media sesión y comprueba que llegue entera, a la
página y a la caché sin señal. Antes el service worker pedía el código con la caché HTTP del
navegador, y durante diez minutos podía arrancar con un app.js nuevo y un módulo viejo; ahora
revalida con el servidor (cache:'no-cache').
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.
El arnés comprueba comportamientos; no dice si un cambio al motor acierta más o menos.
Eso se medía solo con los manuales reales, que no pueden entrar al repo, así que ningún
número del motor era público ni corría en el CI. Para eso está eval/corpus-publico/:
generar.mjs. Están hechos para traer las trampas que encontró el
benchmark con manuales de verdad: títulos de dos renglones, tablas, check lists que
repiten reglas, rótulos sin frase, el mismo título en dos secciones con cifras distintas.lab/gate.mjs carga los cuatro PDF en la app real (lab/volcar.mjs --local, sin
puente ni teléfono, 5 s), califica cada pregunta y la compara con
eval/corpus-publico/linea-base.json. El PR no pasa si una sola pregunta que estaba
bien pasa a mal, o si Hit@3 o MRR@10 bajan más de dos puntos. El informe —qué se rompió,
qué se arregló, McNemar— queda en el resumen del job. Si el cambio arregla preguntas,
npm run gate -- --actualizar sube la línea base y el diff del PR lo enseña.Comprobado con una mutación: pedirle al filtro de solidez una palabra más rompe 7 preguntas y el gate las lista y falla (p = 0.016). La línea base empezó en 68/78 datos y 14/15 «no está», y las 10 que fallaban eran las mismas familias de falla que con los manuales reales: el filtro de solidez que deja sin tarjetas una pregunta con una sola palabra en común, las conversaciones de seguimiento, los rótulos en mayúsculas que se pierden al leer el PDF y un aviso falso por errata. La coincidencia floja de la Fase 3 la subió a 74/78, y el seguimiento que cambia el objeto («¿y las sábanas?») a 81/84 con 6 conversaciones más; quedan dos rótulos en mayúsculas y la errata. El corpus es inventado: protege contra regresiones, no dice cuánto acierta la app en el piso. Eso se mide fuera del repo, con tres baterías separadas (desarrollo, prueba y confirmación, esta última corrida una sola vez), y ahí el modo manual da 58–70 % con preguntas que no vio al afinarse.
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?» → «‹marca X› es la marca propia de ‹la cadena›» «¿cuál es marca preferencial?» → «‹marca X› es la marca preferencial»
Comprobado contra los manuales: «preferencial» no aparece en ninguno de los once, la marca solo en otra sección, 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.
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:
Nada de esto es un problema, pero prefiero decirlo a que se descubra abriendo DevTools:
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.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.docs/adr/0004.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:
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.
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.
El bloque de fuentes se desbordaba en celular. Justo en el único dispositivo donde esto se usa.
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.
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.
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.
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.
Desde la pestaña Revisar, una parte de Veristack ya vive aquí: el mismo principio y los mismos cuatro niveles, pero en el teléfono, sin red y con el % vacío medido de verdad (en Veristack era un dato de entrada).
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.
Código visible, no de uso libre para negocios: PolyForm Noncommercial 1.0.0. Estudiarlo, probarlo y usarlo sin fines comerciales está permitido; cualquier uso por o para una empresa necesita licencia comercial escrita. Detalle en LICENCIA-COMERCIAL.md. Hasta el 5 de octubre de 2026 se publicó con licencia MIT.
GRAVEOBSERVACIÓNALTA, 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.[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.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.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.
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.
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.
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.
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.
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.
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.
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.
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".
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.
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.