# 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`](../../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 `observaciones` → `observacionesInternas`. **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:250` — `SELECT c.*` seguido de `dict(cot)`, sin lista blanca | | Tiene dominio público en producción | `docker-compose.coolify.yml:67` — `SERVICE_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`: ```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. ```sql 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**: ```sql 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 `