Files
cotizador/scripts/verificar-propuesta-validacion.ts
T
urieljarethandClaude Opus 5 bdd9319fd0 Propuesta IA: corregir lo que encontro la auditoria de herramientas y plantillas
La auditoria confirma lo que se pedia verificar: la IA SOLO puede usar las tres
herramientas internas. Una por llamada, ninguna se ejecuta jamas (el input del
modelo solo va a schema.safeParse, no hay despachador), y no se envia ningun
tool del proveedor, web search, ejecucion de codigo, MCP ni beta.

Lo que NO cumplia era la otra mitad, la correspondencia con las plantillas:

CRITICO - el documento cobraba un IVA que la cotizacion no cobra. Cada partida
se imprimia a precio SIN IVA y debajo un unico total CON IVA, rematado con
"Importes con IVA incluido", mientras el PDF economico del mismo correo dice
"los precios no incluyen IVA" sobre las mismas cifras. Las lineas no sumaban su
propio total. Ahora la caja de totales desglosa subtotal / IVA / total como la
plantilla autorizada, las lineas siguen sin IVA igual que el otro documento, y
la nota al pie dice lo que de verdad hacen las lineas.

CRITICO - los avisos bloqueantes no bloqueaban nada. hayBloqueantes() no se
llamaba en ningun sitio y la ruta del PDF nunca leia avisos: el documento del
cliente se descargaba igual con una fuga de notas internas dentro. Ahora
devuelve 409 con la lista de lo que hay que corregir. El anexo interno si se
permite: es justo el que el asesor necesita para arreglarlo.

ALTO - seis campos que SI se imprimen al cliente no pasaban por los filtros de
marca, moneda, garantias y PII: el texto y el autor de la cita destacada, la
metrica y el periodo de cada resultado, quien decide cada pendiente, y el
momento del backlog. textoVisible ahora los cubre y queda documentado que debe
seguir a lo que dibuja el PDF.

ALTO - el plan Bucefalo se caia del documento y de sus totales, aunque si
aparece en el PDF economico y en el Excel: cargarEconomia nunca leia la
relacion. El cliente recibia dos documentos con alcances distintos.

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

124 lines
7.6 KiB
TypeScript

import { validarPropuesta, normalizar, hayBloqueantes, type Aviso } from "@/lib/propuesta/validacion";
import type { PropuestaConsultiva } from "@/lib/propuesta/schemas";
import type { DatosEconomicos } from "@/lib/propuesta/economia";
let fallas = 0;
function check(n: string, ok: boolean, d = "") {
console.log(`${ok ? " OK " : " FALLA"} | ${n}${d ? " -> " + d : ""}`);
if (!ok) fallas++;
}
const tiene = (a: Aviso[], regla: string) => a.some((x) => x.regla === regla);
const CITA = "no llevamos control de quien contesta el whatsapp el fin de semana";
const econ: DatosEconomicos = {
partidas: [{ refPartida: "P01", servicioCotizadoId: "c1", nombre: "Sitio web", fase: 1, tipoPago: "unico", precio: 12000, tiempoEntrega: "2 semanas", modeloCobro: "fijo", horas: null, tarifaHora: null, entregables: [] }],
totales: { subtotalUnico: 12000, subtotalMensual: 0, ivaUnico: 1920, ivaMensual: 0, totalUnico: 13920, totalMensual: 0, totalPrimerAnio: 13920, incluyeIva: true },
moneda: "MXN",
planBucefalo: null,
};
const base = (): PropuestaConsultiva => ({
hechos: {
citas: [{ id: "C01", textoLiteral: CITA, quienLoDijo: "el socio" }],
hechos: [{ id: "H01", enunciado: "No hay control de la atencion", confianza: "confirmado", citas: ["C01"] }],
materialesPendientes: [{ texto: "Accesos al dominio", bloqueaEntrega: true }],
decisionesPendientes: [{ texto: "Definir responsable de atencion", quienDecide: "el socio" }],
mencionesFueraDeAlcance: [],
redFlags: [],
},
diagnostico: {
hallazgos: [{ id: "D01", urgencia: "rojo", titulo: "Prospectos sin seguimiento", descripcion: "Los mensajes del fin de semana se pierden.", confianza: "confirmado", citas: ["C01"], hechos: ["H01"] }],
valorProblema: {
dimensiones: [{ tipo: "tiempo", descripcion: "Horas perdidas de atencion", calculo: { factores: [{ nombre: "horas por semana", valor: 8, confianza: "confirmado" }, { nombre: "semanas", valor: 52, confianza: "confirmado" }, { nombre: "costo por hora", valor: 400, confianza: "estimado" }], montoAnualMXN: 166400 }, confianza: "estimado", hechos: ["H01"] }],
notaMetodologia: "Ocho horas por semana a costo cargado.",
},
resultados: [{ enunciado: "Ningun mensaje sin respuesta en mas de 24 horas", metrica: "tiempo de respuesta", lineaBase: null, periodoMedicion: "mensual" }],
},
redaccion: {
hero: { titulo: "Propuesta de digitalizacion", subtitulo: "Ordenar la atencion para dejar de perder prospectos." },
citaDestacadaId: "C01",
alcance: [{ refPartida: "P01", descripcionResultado: "Un lugar a donde mandar a los prospectos que llegan de los anuncios." }],
beneficios: [{ etiqueta: "Respuesta", texto: "Cada mensaje queda registrado y con responsable.", hallazgoId: "D01" }],
exclusiones: [{ texto: "No incluye migrar el historico de pedidos de los ultimos 4 anos.", razon: "Se evalua en fase 2." }],
backlogEvolucion: [],
notasInternas: ["El socio es quien decide."],
},
});
const fuentes = { textoCliente: `El socio dijo: ${CITA}. Nada mas.`, textoInterno: "" };
check("normalizar quita acentos y puntuacion", normalizar("¿Como estas, Juan?") === "como estas juan");
check("normalizar colapsa espacios", normalizar(" a b ") === "a b");
const limpia = validarPropuesta(base(), econ, fuentes);
check("una propuesta valida no produce bloqueantes", !hayBloqueantes(limpia), JSON.stringify(limpia.slice(0, 2)));
let p = base(); p.redaccion.alcance[0].refPartida = "P99";
check("R2 detecta una partida inventada", tiene(validarPropuesta(p, econ, fuentes), "R2"));
p = base(); p.redaccion.alcance = [];
check("R2b avisa de una partida cotizada y no descrita", tiene(validarPropuesta(p, econ, fuentes), "R2b"));
p = base(); p.redaccion.citaDestacadaId = "C09";
check("R3 detecta una cita inexistente", tiene(validarPropuesta(p, econ, fuentes), "R3"));
p = base(); p.hechos.citas[0].textoLiteral = "esto nunca lo dijo nadie en la reunion";
check("R3 detecta una cita no verificable", tiene(validarPropuesta(p, econ, fuentes), "R3"));
p = base(); p.hechos.citas[0].textoLiteral = "NO LLEVAMOS CONTROL, de quien contesta... el WhatsApp el fin de semana!";
check("R3 tolera acentos, mayusculas y puntuacion", !tiene(validarPropuesta(p, econ, fuentes), "R3"));
p = base(); p.diagnostico.hallazgos[0].citas = []; p.diagnostico.hallazgos[0].hechos = [];
check("R4 degrada un confirmado sin evidencia", tiene(validarPropuesta(p, econ, fuentes), "R4"));
p = base(); p.diagnostico.valorProblema.dimensiones[0].calculo.montoAnualMXN = 999999;
check("R5 detecta aritmetica que no cuadra", tiene(validarPropuesta(p, econ, fuentes), "R5"));
p = base(); p.diagnostico.valorProblema.dimensiones[0].calculo.montoAnualMXN = null; p.diagnostico.valorProblema.dimensiones[0].calculo.factores = [];
check("R5 no se queja si no hay cifra", !tiene(validarPropuesta(p, econ, fuentes), "R5"));
p = base();
p.diagnostico.valorProblema.dimensiones[0].calculo.factores = p.diagnostico.valorProblema.dimensiones[0].calculo.factores.map((f) => ({ ...f, confianza: "estimado" as const }));
check("R5b avisa si el valor anual no tiene ni un factor confirmado", tiene(validarPropuesta(p, econ, fuentes), "R5b"));
check("R5b no avisa si hay al menos un confirmado", !tiene(validarPropuesta(base(), econ, fuentes), "R5b"));
p = base(); p.redaccion.beneficios[0].hallazgoId = "D99";
check("R6 detecta un beneficio colgante", tiene(validarPropuesta(p, econ, fuentes), "R6"));
p = base(); p.redaccion.beneficios[0].texto = "Lo integramos con HubSpot sin problema alguno.";
check("MARCA detecta otro CRM", tiene(validarPropuesta(p, econ, fuentes), "MARCA"));
p = base(); p.redaccion.hero.subtitulo = "Una inversion de 1,800 USD al mes para tu negocio.";
check("MONEDA detecta USD", tiene(validarPropuesta(p, econ, fuentes), "MONEDA"));
p = base(); p.redaccion.beneficios[0].texto = "Te garantizamos la posicion numero uno en Google.";
check("GARANTIA detecta la promesa", tiene(validarPropuesta(p, econ, fuentes), "GARANTIA"));
p = base(); p.redaccion.hero.subtitulo = "El sueldo de quien atiende sale muy caro hoy en dia.";
check("PII advierte sobre tema salarial", tiene(validarPropuesta(p, econ, fuentes), "PII"));
check("PII es advertencia, no bloqueante",
validarPropuesta(p, econ, fuentes).filter((a) => a.regla === "PII").every((a) => a.severidad === "advertencia"));
// ── R0: la fuga de notas internas, que es la razon de que exista ──
const interno = "el socio pidio descuento antes de entender el alcance del proyecto completo";
p = base(); p.redaccion.hero.subtitulo = `Contexto del proyecto: ${interno}.`;
check("R0 detecta la fuga de notas internas", tiene(validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }), "R0"));
check("R0 es bloqueante",
validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }).filter((a) => a.regla === "R0").every((a) => a.severidad === "bloqueante"));
check("R0 no dispara si no hay fuga", !tiene(validarPropuesta(base(), econ, { ...fuentes, textoInterno: interno }), "R0"));
check("R0 sin notas internas no hace nada", !tiene(validarPropuesta(base(), econ, fuentes), "R0"));
// La fuga tambien se detecta en un hallazgo, no solo en el hero.
p = base(); p.diagnostico.hallazgos[0].descripcion = `Observado: ${interno}.`;
check("R0 mira todos los campos visibles, no solo el hero",
tiene(validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }), "R0"));
// Las notasInternas del propio objeto NO deben disparar R0: no se imprimen.
p = base(); p.redaccion.notasInternas = [interno];
check("R0 no se queja de notasInternas (no se imprimen)",
!tiene(validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }), "R0"));
console.log(fallas === 0 ? "\ntodo paso\n" : `\n${fallas} fallas\n`);
process.exit(fallas === 0 ? 0 : 1);