cvool

Cómo funciona

cvool es open source (MIT). Aquí puedes ver exactamente qué pasa cuando subes tu CV.

El pipeline, paso a paso

01

Subes tu CV

Pegas texto o adjuntas un PDF en tu navegador. Si es PDF, se envía al backend donde Claude lo lee nativamente. Tu archivo nunca se guarda en disco.

02

Sanitización de input

El texto pasa por sanitizeInput() que elimina bytes nulos y caracteres de control. Se trunca a 35,000 caracteres máximo. Rate limiting: 7 peticiones/hora por IP.

03

Prompt constitucional

Tu CV se envía a Claude con un system prompt que incluye principios éticos explícitos: no discriminar, no inventar, no inflar scores. El prompt completo está en src/lib/prompts/analyze.txt — es auditable.

04

Claude analiza y reescribe

Un solo API call con temperature: 0 (determinístico). Claude devuelve JSON streamed via SSE con: score (0-100), análisis en 6 dimensiones, fortalezas, mejoras, y un CV completamente reescrito.

05

Validación y render

El backend parsea el JSON, valida que tenga score + analysis + improved_cv, y lo devuelve al frontend. Si Claude no responde JSON válido, se muestra un error honesto.

06

Resultado en tu navegador

Ves tu score, las 6 dimensiones, sugerencias con before/after, y tu CV mejorado listo para copiar. Todo en tu navegador. Nada se almacena.

Stack técnico

FrontendNext.js 15 + React 19 + Tailwind CSS 4
TipografíaGeist + Geist Mono
IAClaude Sonnet 4.6 (Anthropic SDK)
DeployVercel (auto-deploy on push)
AnalyticsVercel Analytics (anónimo)
Base de datosNinguna. Cero. Nada.
Dependencias6 de producción, 0 librerías UI
LicenciaMIT

Principios de diseño

Transparencia radical

El código fuente está público. El prompt de IA está en el repo. Los pesos del scoring están documentados.

Anti-alucinación

Cada sugerencia debe referenciar contenido real del CV. Si algo no está en tu CV, no se menciona.

Cero discriminación

No penalizamos career gaps, caminos no lineales ni educación no tradicional.

Privacidad por diseño

Sin base de datos, sin cuentas, sin cookies de tracking. Tu CV se descarta inmediatamente.

Fork, clona, contribuye

cvool es MIT. Puedes clonarlo, modificarlo, y lanzar tu propia versión. Solo necesitas una API key de Anthropic.

git clone https://github.com/pixan-ai/cvool.git
cd cvool
npm install
echo "ANTHROPIC_API_KEY=tu-key" > .env.local
npm run dev

Mapa técnico

Cómo viaja tu CV por cvool, capa por capa — desde tu navegador hasta Claude y de vuelta.

Vista de 10 segundos

El flujo, de un vistazo

Sin base de datos y sin cuentas. Tu CV entra, se analiza en memoria y se descarta. Todo ocurre en una sola petición que transmite la respuesta mientras se genera.

pegas o subes el CV
page.tsx
valida y envía
/api/analyze
filtra y reenvía
Claude
analiza en streaming
Resultado
se revela token a token
Los cimientos

El stack

Pocas piezas, cada una con un trabajo claro. Cero librerías de UI: todo es HTML, CSS y un puñado de dependencias.

Next.js 15El framework: define las rutas, ejecuta el servidor y compila el sitio.
React 19La interfaz, vive casi entera en una sola página.
Tailwind 4Los estilos, con tokens de color en OKLCH y utilidades.
@anthropic-ai/sdkEl cliente oficial para hablar con la API de Claude.
Claude Sonnet 4.6El modelo que analiza el CV y también extrae el texto de los PDF.
Server-Sent EventsEl canal que transmite la respuesta en vivo, fragmento a fragmento.
partial-jsonLee el JSON cuando aún está a medio escribir, sin romperse.
@vercel/analyticsAnalítica anónima y agregada: cuántas visitas, sin ningún dato personal.
VercelDonde vive el sitio y se ejecutan las funciones del servidor.

El footprint de dependencias

Del stack de arriba, esta es la realidad en paquetes de npm — y tenerlos contados es una ventaja, no una limitación.

Producción · llega al navegador6 paquetes

next · react · react-dom · @anthropic-ai/sdk · partial-json · @vercel/analytics

Solo desarrollo · nunca llega al navegador9 paquetes

typescript · eslint · eslint-config-next · tailwindcss · @tailwindcss/postcss · @types/node · @types/react · @eslint/js · @eslint/eslintrc

Librerías de componentes UI

Ni shadcn, ni Radix, ni MUI. Cada pixel es HTML y CSS escritos a mano.

0

Por qué menos es mejor

  • ·Auditable: una sola persona puede leer y entender todo el código y cada dependencia.
  • ·Más seguro: cada librería es código en el que confías — menos paquetes significan menor superficie de ataque y menos sustos de cadena de suministro.
  • ·Más rápido: menos JavaScript que descargar y ejecutar, así la página se mantiene ligera y carga al instante.
El código

El árbol de src/

Qué hace cada bloque. Abre cada carpeta para ver los archivos y su función.

app/La aplicación: la interfaz y las rutas de API.
page.tsxToda la interfaz en un archivo (~590 líneas): el formulario, los estados, el lector del stream SSE y el reveal progresivo del resultado.
layout.tsxMetadatos, fuentes (Geist), analytics y el schema JSON-LD para buscadores.
globals.cssLos tokens de color OKLCH, el estilo de los <details> y las animaciones. Sin CSS suelto en los componentes.
api/analyze/route.tsLa ruta que habla con Claude: defensas, prompt, streaming y reemisión como SSE. El corazón del backend.
api/parse/route.tsConvierte un PDF en texto plano usando a Claude como extractor.
lib/La lógica: parseo, defensas, idioma y el prompt.
streamParse.tsParser de JSON incremental: arma el resultado mientras llega, sin esperar al final.
cors.tsLista blanca de orígenes permitidos y cabeceras CORS.
rate-limit.tsLimitador en memoria, best-effort: 7 análisis por hora por IP.
i18n.tsLos helpers t() y dimName() y el tipo Lang; la copy vive en content/.
prompts/analyze.txtEl prompt constitucional (~550 líneas): los principios, el scoring y el formato exacto del JSON.
components/Piezas de interfaz reutilizables.
SubLayout.tsxHeader, footer y selector de idioma de las subpáginas (como esta).
PublicCounters.tsxLos contadores públicos y verificables (visitas, CVs analizados).
CvoolBrand.tsx · icons.tsxEl logotipo, los iconos y otros componentes de UI compartidos.
content/ · types/Texto traducible y contratos de tipos.
home.jsonLa copy de la home, en los cinco idiomas.
{about,how,donate,legal}.jsonLa copy de las subpáginas (es/en): about, how, donate, legal.
types/analysis.tsLos tipos AnalysisResult y PartialResult: el contrato del JSON.
El recorrido

El viaje de la información

Diez pasos, desde tu clic hasta el resultado. Cada uno trae su detalle técnico si quieres bajar una capa.

  1. 1

    Pegas o subes tu CV

    Escribes el texto o adjuntas un PDF. Si es PDF, primero pasa por /api/parse, donde Claude extrae el texto.

    Ver el detalle técnico
    handleFile() envía el archivo a /api/parse como multipart. El route manda el PDF a Claude como documento base64 y devuelve texto plano.
  2. 2

    El navegador valida

    Hasta que no hay al menos 50 caracteres, el botón sigue desactivado. Todavía no viaja nada.

    Ver el detalle técnico
    ready = cvText.trim().length >= 50 && !loading && !parsing. Se recalcula en cada render.
  3. 3

    Se envía a /api/analyze

    Una sola petición POST con tu CV (y el puesto objetivo, si lo diste). La respuesta no es un JSON normal: es un stream.

    Ver el detalle técnico
    fetch('/api/analyze', { method:'POST', body: JSON.stringify({ cvText, targetRole }) }). Se lee con res.body.getReader().
  4. 4

    El servidor filtra cada petición

    Antes de gastar un solo token: rechaza peticiones enormes, comprueba el origen, limita la frecuencia y limpia el texto.

    Ver el detalle técnico
    content-length > 4MB → 413 · validateOrigin (allowlist) · isRateLimited (7/h por IP) · clean() quita caracteres de control y recorta a 35.000.
  5. 5

    Se arma el mensaje para Claude

    Tu CV se envuelve en etiquetas para que el modelo lo distinga de las instrucciones, y se le antepone el prompt constitucional.

    Ver el detalle técnico
    system: analyze.txt con cache_control ephemeral. user: <cv_text>…</cv_text> + <target_role> opcional.
  6. 6

    Claude responde en streaming

    El modelo no contesta de golpe: va escribiendo el JSON del análisis token a token, y cada fragmento sale de inmediato.

    Ver el detalle técnico
    anthropic.messages.stream({ model:'claude-sonnet-4-6', max_tokens:8000, temperature:0 }). Se itera sobre content_block_delta.
  7. 7

    El servidor lo reemite como SSE

    Cada fragmento se reenvía al navegador como un evento Server-Sent Event, etiquetado por tipo: fragmento, progreso, resultado o error.

    Ver el detalle técnico
    event: chunk|progress|result|error, seguido de data: {…} y una línea en blanco. Connection keep-alive, Cache-Control no-cache.
  8. 8

    El navegador revela mientras llega

    En vez de esperar al final, el cliente lee el JSON a medio escribir y va mostrando cada sección en cuanto está lista.

    Ver el detalle técnico
    parsePartial() (partial-json) sobre el buffer acumulado → PartialResult. Los números se ocultan hasta estar completos para no parpadear.
  9. 9

    Se valida la forma final

    Cuando el JSON está completo, el servidor comprueba que tenga la estructura correcta antes de marcarlo como resultado.

    Ver el detalle técnico
    isValidResult() verifica score.total (number), summary (string), los arrays de strengths/improvements e improved_cv. Si falla → event:error.
  10. 10

    Resultado final, y nada se guarda

    El CV mejorado y el diagnóstico aparecen. Tu CV vivió solo en memoria durante la petición; al terminar, se descarta.

    Ver el detalle técnico
    setResult() pinta la UI. No hay base de datos: el texto no se persiste en ningún lado.
El núcleo

Cómo llega a Claude

Una sola llamada a la API, en streaming. Esto es, en esencia, lo que recibe el modelo:

anthropic.messages.stream({
  model:       "claude-sonnet-4-6",
  max_tokens:  8_000,
  temperature: 0,
  system:   [{ text: analyze.txt, cache_control: "ephemeral" }],
  messages: [{ role: "user", content: "<cv_text>…</cv_text>" }],
})
modelSonnet 4.6 es una decisión cerrada (comparada con Haiku 4.5 y Opus). La espera depende de cuántos tokens escribe (~5.600 ≈ 100s), no del tamaño del modelo.
temperaturetemperature 0 hace la salida lo más determinista posible: el mismo CV produce un análisis estable.
cache_controlEl prompt del sistema se marca como cacheable (ephemeral): repetirlo abarata el costo, aunque no acelera la generación.
max_tokensmax_tokens 8.000 es un techo de seguridad; un análisis real ronda los 5.600 tokens.
El truco

El streaming, por dentro

Cuatro tipos de evento viajan por el mismo canal abierto:

event: chunkUn fragmento de texto recién generado. Se acumulan para reconstruir el JSON.
event: progressCuántos tokens van; alimenta la barra de progreso. El último trae done: true.
event: resultEl JSON final, ya validado. Es la señal de que el análisis terminó.
event: errorAlgo falló: JSON inválido, forma incompleta, error de la API o límite alcanzado.

El orden en que aparece

El JSON está diseñado para que los campos lleguen justo en el orden en que la interfaz los necesita:

detected_languageinferred_rolescorestrengthsimprovementsimproved_cv

Por eso ves primero el idioma y el puntaje, luego las fortalezas y las mejoras, y al final el CV reescrito creciendo letra a letra.

El porqué

Por qué estas decisiones

Cuatro elecciones que parecen pequeñas y sostienen todo lo demás.

¿Por qué Sonnet 4.6 y no un modelo más rápido?

La espera es output-bound: casi todo el tiempo es el modelo escribiendo ~5.600 tokens. Un modelo más pequeño no lo arregla, así que cambiar de modelo no es una mejora de velocidad.

¿Por qué la función dura hasta 300 segundos?

Un análisis real tarda 60–120s. Si se bajara ese límite, el stream se cortaría a media generación y el resultado nunca llegaría — sin ningún error visible.

¿Por qué SSE y no esperar el JSON completo?

Cien segundos mirando un spinner se sienten rotos. Con SSE ves progreso real y secciones apareciendo; además evita que la petición caduque por timeout.

¿Por qué cualquier fallo se trata como error visible?

Si la respuesta no es OK, el cliente muestra un mensaje en lugar de volver al formulario en silencio. Un fallo mudo es peor que un error claro.

La promesa

Privacidad por diseño

No es una política, es la arquitectura:

  • ·Sin base de datos. El CV existe en memoria solo durante la petición.
  • ·Sin cuentas y sin cookies de seguimiento.
  • ·El limitador de frecuencia vive en memoria; se reinicia y no guarda historial.
  • ·Los contadores públicos (visitas, CVs analizados) no contienen ningún dato personal y cualquiera puede verificarlos.

Eso es todo: una app pequeña, defensas claras, un stream y un prompt cuidado. La elegancia está en lo que no tiene.