Files
cotizador/docs/superpowers/specs/2026-07-28-propuesta-consultiva-ia-fase0-fase1-design.md
T
urieljarethandClaude Opus 5 20f535d485 Retirar el servidor MCP y fijar todas las dependencias del API
MCP: se retira por completo (endpoint, paquete api/app/mcp/ y dependencia).
Tres razones, en orden de peso:

- Nadie lo usa.
- Llevaba roto desde antes de este trabajo. Con credencial valida devolvia 500:
  el handler construia un StreamableHTTPServerTransport nuevo por peticion, sin
  manejo de sesion. Lo que estaba expuesto a internet era la puerta abierta de un
  cuarto averiado.
- Su SDK sin fijar tumbo el API entero en produccion al saltar a 2.0.0.

Retirarlo es una mitigacion mas fuerte que autenticarlo, que fue lo que hizo el
commit anterior. Recuperable con `git show edb500f5:api/app/mcp/server.py`.
Se limpia tambien la ruta "/mcp" que el endpoint raiz seguia anunciando, y las
menciones a MCP del docstring y la descripcion de Swagger.

Dependencias: once de las doce eran rangos `>=` sin techo, o sea que cada
reconstruccion era una tirada de dados contra PyPI. `pydantic>=2.0` habria
aceptado pydantic 3 con la misma alegria con la que `mcp>=1.0.0` acepto 2.0.0.
Ahora todas van fijadas a la version exacta que corre sana en produccion,
capturada con pip freeze del contenedor healthy.

El lado Next.js ya era reproducible via package-lock.json; por eso el web nunca
se cayo durante el incidente y el api si.

Docs actualizados: AGENTS.md, README.md, api/COTIZADOR_API_SKILL.md y el spec,
que ademas registra en su seccion 0 las tres desviaciones de Fase 0 respecto a
lo disenado (el Despliegue B cancelado, la retirada del MCP y la deriva de
dependencia).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 17:51:21 -06:00

29 KiB
Raw Blame History

Propuesta consultiva con IA — Diseño de Fase 0 y Fase 1

Qué es este documento: la especificación técnica cerrada de las dos primeras fases del plugin descrito en docs/BI-propuesta-consultiva-IA.md. Aquel documento es el análisis de negocio y dice explícitamente que no es un spec. Este sí lo es.

Alcance: Fase 0 (recuperar el contexto humano que hoy se descarta, más los bloqueadores que el análisis destapó) y Fase 1 (script local que valida si la IA escribe documentos utilizables). Fases 2-4 quedan fuera y tendrán su propio ciclo.

Fecha: 2026-07-28 · Estado: Fase 0 desplegada en producción · Fase 1 pendiente Base verificada: commit 26e6d3a9


0. Registro de ejecución — Fase 0 (2026-07-28)

Fase 0 está en producción. Tres cosas salieron distinto de lo diseñado y este documento quedaría mintiendo si no se dijeran.

0.1 El movimiento de datos (Despliegue B) se canceló, no se pospuso

El §4.3 describe un UPDATE que mueve observacionesobservacionesInternas. No se va a ejecutar nunca. Al inspeccionar producción antes de migrar, la única fila con observaciones no vacías resultó ser UJ2606AG777 (estado aprobada), y su texto son condiciones de pago dirigidas al cliente en segunda persona: "llevamos una bitácora de horas con acceso para ti; a fin de mes te enviamos el reporte… solo pagas las horas efectivamente trabajadas."

Aplicar el UPDATE conservador habría ocultado condiciones ya acordadas de un documento emitido. Esa fila ya estaba clasificada correctamente. La migración quedó reducida a ADD COLUMN IF NOT EXISTS: cero filas modificadas. Verificado tras el despliegue — conteos 2/3/59/5/2 idénticos al respaldo y cero filas en observacionesInternas.

Lección para el futuro: mirar el dato real antes de diseñar su migración. El diseño "conservador" era el equivocado para estos datos.

0.2 El servidor MCP se retiró en vez de asegurarse

El §4.1 diseñaba Fase 0-A como "aplicar require_auth + lista blanca de columnas". Eso se implementó y se verificó en producción (401 sin credencial). Después se descubrió que, con credencial válida, el endpoint devolvía 500: el código construía un StreamableHTTPServerTransport por petición sin manejo de sesión. Llevaba roto desde antes de este trabajo — lo que estaba expuesto era la puerta abierta de un cuarto averiado.

Dado que nadie lo usaba, se retiró por completo (endpoint, paquete api/app/mcp/ y la dependencia). Es una mitigación más fuerte que autenticarlo. Recuperable con git show edb500f5:api/app/mcp/server.py.

0.3 Una dependencia sin fijar tumbó el API 50 minutos

api/requirements.txt tenía mcp>=1.0.0. La primera reconstrucción en dos semanas tomó mcp 2.0.0, que eliminó Server.list_tools(). El import lanzó AttributeError al arrancar y el contenedor quedó en crash-loop.

Lo agravó que main.py capturara solo ImportError alrededor del montaje del MCP: un AttributeError se escapó y tumbó también el REST, por un componente opcional.

Once de las doce dependencias eran rangos >= sin techo. Todas se fijaron a versión exacta — las que corrían sanas en producción. El web (Next.js) nunca estuvo caído: tiene package-lock.json y es reproducible.


1. Por qué el alcance es este

El documento de negocio describe ocho subsistemas. Construirlos de una vez significa un mes de trabajo antes de saber si la tesis funciona. Su propio §13 argumenta la salida:

"El entregable real no es el script: son 5-10 documentos generados sobre cotizaciones reales, revisados por el asesor. Si esos documentos no son buenos, nada de lo demás importa y hay que iterar el prompt, no construir infraestructura."

Se adopta ese criterio. Fase 1 termina cuando el asesor puede decir "esto lo mandaría a un cliente después de editarlo 10 minutos" — o cuando queda claro que no.

El análisis previo a este spec encontró cuatro problemas que el documento de negocio no anticipaba. Tres son bloqueadores y entran al alcance; el cuarto cambia dónde vive un dato.


2. Hallazgos que modifican el plan original

Los cuatro están verificados en disco, no inferidos.

2.1 🔴 El endpoint MCP publica todas las cotizaciones sin autenticación

Eslabón Evidencia
POST /mcp no valida nada api/main.py:107 — sin un solo Depends(). Cero coincidencias de require_auth en el archivo
La función de auth existe y no se usa ahí api/app/auth.py:45 define require_auth
La herramienta devuelve la fila completa api/app/mcp/server.py:250SELECT c.* seguido de dict(cot), sin lista blanca
Tiene dominio público en producción docker-compose.coolify.yml:67SERVICE_FQDN_API_8000

El agujero ya existe: precios, catálogo y datos de clientes son legibles hoy por cualquiera. Lo relevante para este spec es que SELECT c.* publicaría la columna nueva de notas internas sin que nadie toque server.py. Entra al alcance como Fase 0-A, previa a todo lo demás.

2.2 🟠 No existe una fuente de verdad para los totales

La regla de validación §8.3.1 del documento de negocio exige comparar contra "el total de la cotización en BD". No hay tal cosa:

  • Ninguna función devuelve los totales de una cotización. El cálculo está duplicado con .filter().reduce() en siete consumidores (PDF, Excel, detalle, PreciosEditables, el formulario, la lista y el dashboard), con criterios que no coinciden.
  • El IVA está hardcodeado como * 1.16 en pdf-generator.ts:328-329 y excel-builder.ts:377-378, ignorando IVA_RATE.
  • El flag Cotizacion.incluirIva no se respeta en esos cálculos.

Hoy el PDF y el Excel de una misma cotización pueden discrepar y nada lo detecta. Entra como Fase 0-B.

2.3 🔴 El Excel es un documento del cliente, no una herramienta interna

Este hallazgo corrigió un supuesto equivocado del diseño inicial, que proponía poner las notas internas en el Excel.

Evidencia Ubicación
Banner con la razón social de E3 excel-builder.ts:165
"En atencion a:" + nombre del cliente excel-builder.ts:181
Nota legal de precios, IVA y vigencia excel-builder.ts:400
Razón social + domicilio fiscal al pie de cada hoja de detalle excel-builder.ts:539-547
Nombre de archivo {empresa} - {cliente} - {numero}.xlsx src/app/api/export/excel/[id]/route.ts:76
Botón en la misma fila que Preview PDF y PDF src/app/(app)/cotizaciones/[id]/page.tsx:93-95

Escenario de fallo: el asesor exporta ambos, caen en Descargas con el mismo prefijo, los arrastra juntos al correo, y el cliente abre la última pestaña y lee el plan de recorte de precio y las red flags sobre él mismo.

Consecuencia: en Fase 0 las notas internas no entran a ningún exportador. Viven solo en la app. Es la misma garantía estructural que ya tiene el PDF.

2.4 🟠 Las transcripciones se sincronizarían a la nube de MEGA

El repo vive en H:\MegaSync\Proyectos\Cotizador. El .megaignore actual excluye únicamente .next, node_modules y .turbo. Cualquier transcripción guardada dentro del repo se sube a la nube: nombres de empleados, comentarios de desempeño, cifras salariales. .gitignore protege el repositorio, no la sincronización.

Consecuencia: contexto/ y salidas/ van a .gitignore y a .megaignore, y la exclusión se verifica empíricamente antes de colocar material real.


3. Decisiones cerradas

Ninguna de estas se re-litiga durante la implementación.

# Decisión Resolución Razón
D1 Alcance del ciclo Fase 0 + Fase 1 Validar calidad antes de construir infraestructura
D2 Separar interno de visible Campo nuevo observacionesInternas; observaciones pasa a ser texto del cliente Hace la fuga estructuralmente imposible en vez de depender de un marcador que se olvida
D3 Dónde viven las notas internas Solo en la app. Ningún exportador §2.3
D4 Forzar el schema sin structured outputs Tool-calling: input_schema de la herramienta = contrato. Zod valida siempre MiniMax no soporta output_config
D5 Proveedor MiniMax-M3 vía endpoint compatible con Anthropic Decisión del negocio (Token Plan contratado)
D6 Sanitización de PII Filtro de salida solamente El modelo necesita el contexto completo para diagnosticar; el riesgo real es lo que llega al cliente
D7 Seguridad del MCP Cerrar el agujero primero: require_auth + lista blanca de columnas §2.1
D8 Totales Función canónica calcularTotalesCotizacion() + migrar PDF y Excel §2.2
D9 Base del ratio precio/valor Primer año con IVA (único + mensual × 12) Es el desembolso real a 12 meses; hace comparable el ratio contra un valor anual
D10 Nombres de empleados detectados Advertir, no bloquear Un bloqueo heurístico produce falsos positivos constantes y enseña a ignorar la alerta
D11 Ubicación de transcripciones Dentro del repo, en .gitignore y .megaignore §2.4
D12 Migración de datos Dos despliegues: A aditivo, B mueve datos §4.3

4. Fase 0 — Recuperar el contexto humano

Cuatro bloques. A y B son prerrequisitos de C.

4.1 Fase 0-A · Cerrar el MCP

Superado por §0.2. Esto se implementó tal cual y se verificó (401 sin credencial), pero después el servidor MCP se retiró por completo. Se conserva el diseño original porque documenta el agujero que existía y por qué importaba.

  1. Aplicar require_auth (api/app/auth.py:45) al endpoint POST /mcp en api/main.py:107.
  2. Sustituir el SELECT c.* de _obtener_cotizacion (api/app/mcp/server.py:250) por una lista blanca de columnas explícita, con el mismo criterio que el REST ya aplica vía CotizacionResponse (api/app/models/cotizacion.py:130-152).
  3. Auditar el resto de server.py en busca de otros SELECT * y darles el mismo tratamiento.

Criterio de aceptación: una petición POST /mcp sin credencial devuelve 401, y obtener_cotizacion con credencial válida no incluye observacionesInternas en su salida.

4.2 Fase 0-B · Totales canónicos

Escribir en src/lib/calculators.ts:

calcularTotalesCotizacion(cotizacion): {
  subtotalUnico, subtotalMensual,
  ivaUnico, ivaMensual,
  totalUnico, totalMensual,
  totalPrimerAnio,        // totalUnico + totalMensual * 12, con IVA — base del ratio (D9)
  moneda
}

Reglas: usa IVA_RATE, respeta Cotizacion.incluirIva, y solo suma partidas con seleccionado: true. Para cotizaciones dobles se apoya en calcularTotalesOpcion.

Migrar a ella pdf-generator.ts y excel-builder.ts, eliminando los * 1.16. Los otros cinco consumidores se migran en un ciclo posterior; se documenta la deuda.

Criterio de aceptación: para un conjunto de cotizaciones reales, el total del PDF y el del Excel coinciden dígito por dígito, y una cotización con incluirIva: false no muestra IVA en ninguno.

4.3 Fase 0-C · El campo de contexto humano

Despliegue A — solo aditivo.

  • ALTER TABLE "Cotizacion" ADD COLUMN "observacionesInternas" TEXT; — sin UPDATE.
  • El formulario muestra dos textareas visualmente inconfundibles: "Observaciones (las ve el cliente)" y "Notas internas (no salen de la app)", con badge de color distinto.
  • La vista de detalle muestra ambos campos; el histórico aparece etiquetado como "Observaciones (histórico, sin clasificar)".
  • El PDF imprime solo observaciones. CotizacionPDFData (pdf-generator.ts:31-53) no declara observacionesInternas — la garantía es estructural, no disciplinaria.
  • Ningún exportador recibe el campo interno.

Despliegue B — movimiento de datos. CANCELADO, ver §0.1. El UPDATE de abajo no se ejecutó ni se va a ejecutar: la única fila afectada contenía texto dirigido al cliente y ya estaba donde debía. Se conserva por si algún día aparece una instalación con datos mal clasificados.

UPDATE "Cotizacion"
   SET "observacionesInternas" = "observaciones",
       "observaciones" = NULL
 WHERE "observaciones" IS NOT NULL
   AND btrim("observaciones") <> ''
   AND "observacionesInternas" IS NULL;   -- guarda de idempotencia, obligatoria

Antes de correr B, exportar y guardar fuera de MegaSync:

COPY (SELECT id, numero, estado, observaciones FROM "Cotizacion"
      WHERE observaciones IS NOT NULL AND btrim(observaciones) <> '')
TO STDOUT WITH CSV HEADER;

Ese CSV es el down que Prisma no da: no hay migraciones de reversa en el repo ni servicio de backup en docker-compose.coolify.yml.

Por qué dos despliegues. Si A mueve los datos y el despliegue falla, Coolify redespliega la imagen anterior, el código viejo lee observaciones (NULL en el 100% de las filas) y el asesor ve todas sus notas desaparecidas — sin error, sin log, en el momento de máximo estrés.

Puntos de integración del campo nuevo (rastreados de punta a punta): prisma/schema.prisma, src/lib/schemas.ts (los dos schemas), src/lib/store.ts, CotizacionForm.tsx, POST /api/cotizaciones, PUT /api/cotizaciones/[id], cotizaciones/[id]/page.tsx, cotizaciones/[id]/editar/page.tsx, y en Python los tres modelos Pydantic (CotizacionCreate, CotizacionUpdate, CotizacionResponse) más el _add del PUT.

Los INSERT de Python no son urgentes: la columna es nullable y Postgres pone NULL. Los modelos Pydantic sí lo son — hay precedente demostrado de que sin declararlos el campo nunca sale del API (incluirIva existe en la tabla desde 20260630000000 y el API Python no lo devuelve).

Deuda declarada: zod se importa en src/lib/schemas.ts:1 y resuelve transitivamente a 4.3.6, pero no está en package.json. Se declara explícitamente (npm i zod@^4.3.6); un npm ci --omit=dev revienta hoy sin eso, y Fase 1 depende fuertemente de zod.

4.4 Fase 0-D · Bugs vecinos aprobados

Bug Arreglo
Orden de partidas Ninguna consulta tiene orderBy en servicios, y drawSection (pdf-generator.ts:216-268) asume que vienen agrupadas por fase. Añadir orderBy: [{fase}, {createdAt}] en las cuatro rutas de export
Datos bancarios en borrador export/pdf/route.ts:45 llama solo a getConfigBranding(); el Excel llama a ambas. Añadir getConfigBancaria()
Partida por demanda en $0 Las copias locales de detalleModelo en pdf-generator.ts:21-29 y excel-builder.ts:58-66 no manejan modeloCobro === "demanda"; la canónica de calculators.ts:23-41 sí. Usar la canónica
Bonos con dos redacciones pdf-generator.ts:377-384 tiene seis bonos hardcodeados y la tabla Bono del seed dice otra cosa; la tabla no se consulta en src/. Definir la tabla como fuente de verdad

5. Fase 1 — Script local de validación de calidad

Sin base de datos nueva, sin UI, sin despliegue. Un script que lee una cotización y una transcripción, corre el pipeline, y escribe un JSON, un HTML y un reporte.

5.1 Contratos compartidos

Los cuatro diseños explorados compartían vocabulario pero no contratos. Estas resoluciones se escriben una vez, en un solo archivo de tipos que todos importan. Sin esto el sistema arranca, no falla, y valida el vacío.

# Conflicto Resolución
B1 Clave de join partida ↔ IA refPartida, formato /^P\d{2}$/"P01". El cuid de ServicioCotizado nunca sale hacia el proveedor: no es estable entre ediciones porque el PUT hace deleteMany + createMany
B2 Forma de la evidencia Arrays de IDs (citas[], hechos[]), no prosa. Auditable por máquina, que es el punto de P3
B3 Cita literal Referenciada por ID. Se verifica en el paso 1, donde vive el texto, y su fallo bloquea antes de gastar los pasos 2 y 3
B4 Árbol de módulos src/lib/propuesta/{ia,validacion,render}/, entrada scripts/propuesta-consultiva.ts, tipo raíz PropuestaConsultiva
B5 Códigos de salida 0 limpio · 1 requiere revisión · 2 bloqueado · 3 error del proveedor · 64 error de uso
B6 Variables de entorno Solo MINIMAX_API_KEY / MINIMAX_BASE_URL / MINIMAX_MODEL, pasadas explícitas al constructor. Nunca prefijo ANTHROPIC_*: el SDK las lee por su cuenta y un ANTHROPIC_BASE_URL exportado en la shell mandaría la clave de MiniMax a Anthropic
B7 tool_choice No se envía en Fase 1. Fijarlo solo en el reintento le da al reintento un prefijo distinto, o sea que se paga el contexto completo justo cuando es más grande
B8 Snapshot del catálogo Sin id y sin precioBase. La resolución es por nombre + fase, reportando ambigüedad — ServicioCatalogo.nombre no es @unique
B9 Valor anual del problema dimensiones[].calculo.montoAnualMXN. El total se suma en código, nunca lo emite el modelo

5.2 El corte: qué copia el código y qué genera el modelo

La protección más fuerte del principio P1 no es una regla de validación: es que ningún schema que llena el modelo contiene un solo campo de dinero de la cotización.

  • El modelo devuelve: refPartida + prosa (diagnóstico, resultados, descripciones en lenguaje de beneficio, exclusiones, pendientes, backlog).
  • El código inyecta después: precios, totales, IVA, número de cotización, vigencia, fechas.

Así P1 deja de ser algo que alguien puede olvidar validar y pasa a ser estructuralmente imposible de violar. El ratio precio/valor también lo calcula el código (D9) y se imprime solo en el anexo interno.

5.3 El pipeline de tres pasos

Paso Entrada Salida Por qué separado
1 · Extracción Transcripción + notas + observaciones Hechos: dolores, cifras con confianza, citas literales con ID, materiales, decisiones, red flags, menciones fuera de alcance Determina la calidad de todo lo demás. Es corregible por el asesor antes de propagarse
2 · Diagnóstico Hechos del paso 1 + cotización + catálogo Hallazgos con semáforo, valor del problema, resultados a lograr Razonamiento, no redacción
3 · Redacción Salidas de 1 y 2 PropuestaConsultiva Con el análisis hecho, esto es escritura

Cada paso define una herramienta cuyo input_schema es su contrato, derivado del Zod con z.toJSONSchema nativo (verificado: preserva additionalProperties y description, inlinea subschemas, omite superRefine, y emite un $schema que hay que borrar). No hace falta zod-to-json-schema.

5.4 El presupuesto de reintentos

Hallazgo verificado que ningún diseño anticipó: .superRefine no corre si la forma falla. Eso crea cuatro compuertas secuenciales:

  1. Forma Zod — siempre
  2. superRefine (IDs únicos, refs internas) — solo si 1 pasa
  3. Validación cruzada de runtime (refPartida reales) — solo si 2 pasa
  4. Filtro de contenido y PII — solo si 3 pasa

Con tres intentos y cuatro compuertas, el modo de fallo más probable del piloto es "tres llamadas pagadas, cero documento" — justo lo que Fase 1 no puede permitirse, porque su criterio de éxito es que el asesor lea algo.

Diseño adoptado:

  • El filtro de contenido y PII corre sobre el objeto crudo aunque Zod haya fallado, recorriendo rutas parciales. Los hallazgos de las cuatro compuertas se acumulan en un solo mensaje de corrección.
  • Presupuesto: 3 intentos totales por paso (1 inicial + 2 reintentos). El paso 3 recibe 4 intentos totales (1 + 3) por ser el más largo y el que atraviesa más compuertas.
  • Al agotarse los intentos se escribe igual el HTML del cliente si y solo si no hay bloqueantes de PII, moneda, garantía ni marca. Salir con código 1, no con 2.
  • En la rama tool_incorrecta hay que emitir un tool_result con is_error: true por cada tool_use del turno antes del texto de corrección; un mensaje de usuario plano después de un tool_use produce un 400.

5.5 Validación post-generación

Las seis reglas de §8.3 del documento de negocio, más una nueva que el análisis de riesgo destapó.

Regla Qué comprueba Al fallar
R0 (nueva) Ningún n-grama de 7 tokens de observacionesInternas ni de ServicioCotizado.notas aparece en el HTML del cliente Bloquea
R1 Todo monto en prosa pertenece al conjunto de la economía (BD) o al del valor (declarado y auditado por R4) Bloquea
R2 Todo refPartida existe en ServicioCotizado. Ninguna partida nueva Bloquea
R3 Cada cita literal existe en las fuentes, con normalización de 9 pasos (NFD sin diacríticos, minúsculas, puntuación, espacios, muletillas de ASR). Corre en el paso 1 Bloquea si cruza hablantes
R4 Todo hallazgo confirmado tiene citas.length + hechos.length > 0. Corre sobre el objeto del paso 2 Degrada a por_validar
R5 El bloque interno se separa antes de renderizar el documento del cliente Bloquea
R6 Sin valor anual, ninguna afirmación de ratio en la prosa Bloquea la afirmación

R0 existe porque las otras seis no la cubren. R5 compara el bloque interno que produjo el modelo contra el HTML; nunca compara observacionesInternas, que es una entrada que el modelo recibe en el prompt y puede copiar literalmente en cualquier campo de prosa.

Filtro de PII de salida (advertencia, no bloqueo — D10): montos en USD, promesas de resultado garantizado, CRMs que no sean Bucéfalo, datos de empleados, temas salariales o legales. Los patrones heurísticos se marcan como tales en el reporte, con su extracto, para que el asesor juzgue. Se debe añadir a la cosecha de nombres las etiquetas de hablante del VTT y los dos campos de notas, que hoy quedarían fuera.

5.6 El renderizador HTML

Es el entregable primario y ningún diseño lo tenía. Primer paso de Fase 1: copiar D:/Documents/plantilla_cotizacion.html (27,100 bytes, hoy fuera del repo) a src/lib/propuesta/render/plantilla.ts y congelar su CSS. Sin esto, Fase 1 termina con tres JSON impecables y nada que enseñar.

Cambios obligatorios sobre la plantilla:

Problema Arreglo
12 rgba() con canales escritos a mano duplican --blue, --cream, --green, --amber Parametrizar de verdad, o color_primario produce un documento con dos azules
.section { page-break-inside: avoid } en secciones enteras Con 15-20 partidas empuja la sección a hoja nueva y la parte igual. Aplicar a filas, no a secciones
No hay @page Todo PDF impreso lleva localhost:3000/... estampado al pie de una propuesta comercial
Tema oscuro + "Gráficos de fondo" desactivado por defecto en Chrome El PDF sale con texto crema sobre blanco. Necesita un modo de impresión claro
Cero hueco para el logo El Anexo A del documento de negocio lo mapea como si existiera
.plan-grid fijo a 1fr 1fr La sección de opciones pide tres tarjetas (anclaje A → B → C)
Números de sección escritos a mano con cinco secciones condicionales Un documento sin mantenimiento salta del 04 al 06. Numerar en el renderizador
"Valor del problema" pide tabla de cuatro columnas No hay patrón CSS; hay que crearlo

Estrategia: template literals puros, sin motor de plantillas. El proyecto no tiene ninguno instalado y no lo amerita.

Inyección CSS: PUT /api/configuracion filtra las claves permitidas pero no valida los valores, y esos valores terminan dentro de un <style>. Validar /^#[0-9a-fA-F]{3,8}$/ en el punto de inyección y caer al default del seed si no cumple.

Anexo interno: archivo separado, INTERNO-NO-ENVIAR.html, visualmente inconfundible. Nunca una sección oculta ni display: none.

5.7 Ergonomía del script

npx tsx scripts/propuesta-consultiva.ts \
  --cotizacion UJ2607UJ003 \
  --transcripcion contexto/UJ2607UJ003/reunion.txt \
  --salida salidas/UJ2607UJ003/

Banderas: --cotizacion --transcripcion --salida --desde-paso --solo-validar. Nada más. --desde-paso y --solo-validar se ganan su lugar: son la diferencia entre iterar el prompt del paso 3 veinte veces o cuatro.

Artefactos: propuesta.html, INTERNO-NO-ENVIAR.html, propuesta.json (con la traza embebida), REPORTE.md, y traza/pasoN.json porque --desde-paso los necesita.

Consola: los tres pasos tardan; imprime progreso por paso, tokens consumidos y aciertos de caché.

contexto/ y salidas/ van a .gitignore y a .megaignore (D11), con la exclusión verificada empíricamente antes de colocar material real.

5.8 Prompts y caché

Tres capas por estabilidad: estable (filosofía, frameworks, reglas, contrato, ejemplos), semi-estable (catálogo), volátil (cotización y transcripción). Dos breakpoints de cache_control.

MiniMax soporta caché con cache_control (4 breakpoints, TTL 5 min, verificable con usage.cache_read_input_tokens), pero el prefijo arranca en tools, no en system.

Invalidadores a prevenir: new Date() en la capa 1, JSON.stringify con orden de llaves no determinista, el número de cotización en la capa estable, cambio de modelo a media ejecución. Serializar el catálogo con orden explícito (orden, id).

Los ejemplos few-shot deben validar contra su propio schema Zod al arrancar el script. Cuesta diez líneas y es el único test que este proyecto va a tener. Un ejemplar defectuoso no produce un error: produce N documentos parecidos entre sí y se diagnostica tarde. El análisis encontró tres defectos en los ejemplos propuestos —uno que no valida, uno que mete el nombre de pila de una empleada en un campo del cliente, y uno que dispara el filtro de montos con "400 pesos"— que hay que corregir antes de la primera corrida.

También hay que pasar el texto fijo de la plantilla por el catálogo de PII una vez antes de la primera corrida, o la primera ejecución devolverá bloqueos que vienen del footer y no del modelo.


6. Supuestos sin verificar

Se documentan porque el diseño se apoya en ellos y la primera corrida debe medirlos.

# Supuesto Qué hacer
V1 MiniMax devuelve cache_read_input_tokens con tools fijo y 2 breakpoints Primera medición obligatoria. Si no cachea, mandar las tres herramientas en cada petición es costo puro
V2 Con stop_reason: "max_tokens" el SDK entrega input como objeto parcial Probar a propósito con max_tokens: 200
V3 countTokens existe en el endpoint compatible Degradar a estimador local y marcar las cifras como estimadas en la traza

Verificados durante el análisis, sin acción pendiente: z.toJSONSchema preserva lo necesario; z.prettifyError emite multi-error con ruta; tsx resuelve el alias @/; core.autocrlf=true exige normalizar saltos de línea.


7. Riesgos aceptados

Riesgo Por qué se acepta Mitigación
La transcripción cruda con datos de empleados viaja a MiniMax El modelo necesita el contexto para diagnosticar (D6) Queda escrito aquí, no solo en la conversación. El filtro de salida protege el documento, no el tránsito
El PDF de una cotización ya enviada deja de ser reproducible Fase 0 cambia el renderizador; el sistema no archiva los PDF emitidos Que el asesor archive lo enviado. Documentar en AGENTS.md que los exportadores no son reproducibles en el tiempo
El hábito del asesor no migra solo Tras Fase 0, el textarea donde lleva meses escribiendo contexto interno pasa a imprimirse en el PDF Avisar por el canal del equipo, no solo cambiar la etiqueta
Fase 1 añade una tercera superficie solo en TypeScript AGENTS.md ya exige paridad entre dos backends Documentar la divergencia. Si Fase 4 expone esto por MCP, la validación tendrá que existir del lado Python

8. Criterios de éxito

Fase 0 — verificable de inmediato:

  • El endpoint /mcp ya no existe (§0.2). Se cumplió antes con 401 verificado; la retirada lo supera.
  • api/requirements.txt fija versiones exactas y el API arranca sin el paquete mcp (§0.3).
  • El total del PDF y el del Excel coinciden dígito por dígito en cotizaciones reales.
  • Una cotización con incluirIva: false no muestra IVA en ningún exportador.
  • Las notas internas no aparecen en ningún archivo exportable.
  • El texto del cliente aparece en el PDF, con saltos de línea correctos y sin romper el footer.

Fase 1 — el criterio es cualitativo y es el que importa:

El asesor revisa 5-10 documentos generados sobre cotizaciones reales pasadas y dice "esto lo mandaría a un cliente después de editarlo 10 minutos."

Métricas de apoyo: cero afirmaciones sin evidencia, cero citas no verificables, y tasa de edición del asesor por debajo del 30%.

Si el criterio no se cumple, la conclusión correcta es iterar el prompt — no construir Fase 2.


9. Fuera de alcance

Modelos PropuestaIA y ContextoCotizacion; carga de archivos en la UI; route handler con patrón de job; vista de revisión con semáforo de salud; dashboard de BI agregado; herramienta MCP generar_propuesta_consultiva; sugerencia de esquema de 2-3 opciones; alerta de subcotización.

También difierido: migrar los cinco consumidores restantes de totales duplicados, y el semáforo de salud de §5.1 del documento de negocio — es una función de producto, no una validación, y no hace falta para saber si el texto generado sirve.