Volver a proyectos
En producción · API + cliente web
Caso de estudio

Un puntaje de CV que se puede auditar.

BuildCv puntúa una hoja de vida contra una oferta concreta y explica de dónde salió cada punto. Sin modelo de lenguaje en ninguna parte del cálculo: los pesos, los umbrales y las reglas están publicados, y el mismo par CV/oferta devuelve el mismo número hoy y el mes que viene.

1.418 métodos de test
4 capas con test propio
47 endpoints
v4 versión del modelo
01 — Contexto

El problema no es el rechazo. Es no saber por qué.

Quien busca empleo manda decenas de postulaciones y recibe silencio. Las herramientas que prometen 'optimizar tu CV para el ATS' devuelven un número sin explicación, y cuando hay un LLM detrás, el mismo CV puede dar 68 hoy y 74 mañana sin que nada haya cambiado. Un puntaje que no se puede reproducir no es un diagnóstico: es una opinión con formato de dato.

  • El puntaje se calcula con aritmética pura en C#. Cero llamadas a un modelo de lenguaje en la ruta de scoring — verificable buscando en el repo.
  • Los pesos por sección son públicos y suman 1.00. El usuario puede recalcular su propio puntaje a mano si quiere.
  • Cada recomendación reejecuta la fórmula completa con esa carencia cubierta y reporta la diferencia real, no una estimación de cuánto podría ayudar.
  • Producto gratuito para buscadores de empleo hispanohablantes, que es la población con menos herramientas serias disponibles.
02 — Arquitectura

Cuatro capas, cada una con su propio proyecto de test.

Arquitectura hexagonal real, no carpetas con nombres de capas. Las dependencias apuntan hacia adentro: el dominio no conoce a la base de datos ni a HTTP. El número a la derecha de cada capa es su cantidad de métodos de test.

DOMINIO 291 tests

BuildCv.Domain

Reglas de negocio puras

Las entidades, los pesos y las bandas. Sin dependencias hacia afuera: acá vive la definición de qué significa que un CV coincida con una oferta, y se puede testear sin levantar nada.

C#.NET 10xUnit
APLICACIÓN 425 tests

BuildCv.Application

Casos de uso · CQRS

El motor de scoring y el constructor de recomendaciones. Orquesta el dominio y define los puertos que la infraestructura implementa.

C#MediatR-style handlersxUnit
INFRAESTRUCTURA 388 tests

BuildCv.Infrastructure

Persistencia y adaptadores

Implementa los puertos: repositorios, migraciones, cifrado. Sus tests corren contra un SQL Server real levantado con Testcontainers, no contra un doble.

EF CoreSQL ServerTestcontainers
API 314 tests

BuildCv.Api

Minimal API · 68 endpoints

Superficie HTTP. Segura por defecto: la política de autorización global exige sesión y hay que optar por salir explícitamente con AllowAnonymous.

ASP.NET CoreOpenAPIRate limiting
CLIENTE Repo aparte

buildcv-v2-web

Next.js · patrón BFF

El navegador nunca habla con la API. Los tokens viven en cookies httpOnly de este origen y cada llamada pasa por un route handler que hace de backend-for-frontend.

Next.js 15TypeScriptPlaywright
03 — El modelo

Los pesos están publicados. Ese es el punto.

Seis secciones, cada una con una porción fija del total. La barra es esa porción: Skills ocupa el 45% de la barra porque pesa 0.45 del puntaje. Cualquiera puede tomar estos números y verificar su propio resultado.

Pesos por defecto · modelo v4 · suman 1.00
Skills 0.45
Experience 0.20
Education 0.10
Certifications 0.10
Languages 0.10
Projects 0.05
Bandas · los cortes, dichos en vez de insinuados
  • Low 0 – 39
  • Medium 40 – 59
  • Good 60 – 79
  • Strong 80 – 100

Una sección que la oferta no pide no consume peso: su porción se redistribuye proporcionalmente entre las que sí pide, así el techo es 100 para toda oferta. La versión anterior le daba 0.5 neutro a una sección no preguntada, y eso volvía inalcanzable la mitad de su peso — un CV impecable sacaba 95 sin que el candidato pudiera averiguar por qué. En un producto cuyo propósito es explicar el puntaje, eso era el peor bug posible.

04 — Explicabilidad

Cada sugerencia trae un número medido.

Cuando BuildCv sugiere un cambio, no estima el beneficio: reejecuta la fórmula completa con esa única carencia cubierta y reporta la diferencia. Un '+3.2 puntos' es un puntaje que se calculó, no un pronóstico. Si el motor no puede medirlo, no lo afirma.

  • El impacto se calcula en 0..1 y se muestra como puntos sobre 100, con un decimal. La precisión de la unidad refleja la precisión del cálculo.
  • El cliente nunca reimplementa una regla del servidor. El motor reconoce alias — 'React.js' satisface 'React' — así que una comparación local contradiría el puntaje que tiene al lado.
  • Una sección con peso 0 muestra 'no medida', nunca un puntaje. El peso es la señal de si aplica; no hay un flag aparte que pueda desincronizarse.
  • Los dos puntajes — coincidencia y legibilidad — son modelos distintos y jamás se suman. Promediarlos daría un número que no describe nada.
05 — Seguridad

Decisiones medidas, no asumidas.

Autorización
Segura por defecto: la política global exige sesión. Salir es explícito con [AllowAnonymous] — lo contrario de olvidarse de proteger un endpoint nuevo.
Límite de tasa
5/min por IP en autenticación, 100/min global. 429 con Retry-After y cuerpo ProblemDetails.
Tamaño de cuerpo
413 antes de bufferizar la petición. Un POST de 121 MB sin autenticar llevaba el contenedor de 56 a 305 MiB. Ahora responde 413 en 11 ms.
Ingress
Sólo Cloudflare. El origen crudo responde 403. El edge no se puede esquivar, y el script de despliegue lo re-verifica.
CSRF
Chequeo de Origin en el middleware del cliente. azurecontainerapps.io no está en la Public Suffix List, así que SameSite no alcanza: cualquier Container App sería same-site con este.
Sesión
Dos cookies httpOnly en el origen del BFF. El navegador nunca sostiene una credencial de la API.
06 — Pruebas

1.418 métodos, y ninguno apagado.

1.418 métodos de test
0 tests omitidos
0 TODO en 334 archivos
3 trabajos de CI
  • Cada capa tiene su proyecto de test: Domain 291, Application 408, Infrastructure 388, Api 302.
  • Los tests de integración corren contra un SQL Server real con Testcontainers. Un doble se escribe desde la misma creencia que estaba equivocada.
  • compose-smoke levanta la topología entera — base, migrador y API — registra un usuario de verdad y verifica que sobreviva a un reinicio.
  • docs/api-contract.md está validado por tests, así que la documentación no puede desincronizarse del código en silencio.
  • El cliente web suma Playwright: humo contra una API real y accesibilidad WCAG AA sobre las pantallas públicas, que corre en CI porque no necesita backend.
07 — Despliegue

Verificar el artefacto no es verificar el despliegue.

Que el código compile, que la imagen se comporte y que lo que está corriendo sea lo que subiste son tres afirmaciones distintas. Las tres se verifican por separado, y la tercera fue la que más sorpresas dio.

  • El script de verificación establece qué revisión está midiendo antes de medir nada: espera a que una sola revisión tenga el 100% del tráfico y se niega a medir durante un rollout.
  • Sin eso casi reporto rota una corrección que estaba bien: el health respondía, pero era la revisión anterior la que contestaba mientras la nueva arrancaba.
  • El período de gracia de apagado se midió en vez de suponerse: una petición que termina dentro de la ventana responde completa y sale con 0; una que la excede recibe SIGKILL y el cliente se queda con una conexión cortada sin status.
  • La app corre con min-replicas 0, así que el verificador la despierta con paciencia antes de medir — un arranque en frío se leyó como despliegue muerto en la primera corrida.
  • La imagen se publica a GHCR sólo desde main y sólo después de que la verificación del contenedor pasó. Se despliega el SHA, nunca latest.
08 — Estado

Qué está hecho y qué falta.

En producción
  • Motor de scoring determinista con modelo versionado (v4)
  • Puntaje de legibilidad, disponible sin necesidad de una oferta
  • Recomendaciones con impacto medido, no estimado
  • API y cliente web desplegados detrás de Cloudflare
  • Página pública de entrada con el modelo completo publicado
En camino
  • Datos del operador en las páginas legales — los únicos campos que el código no puede completar solo
  • Prueba sin cuenta, para decidir antes de registrarse
  • Publicación de la cobertura de código, que hoy se recolecta pero no se sube a ningún lado
  • Job de CI para el contrato OpenAPI, pendiente de una regeneración contra la API viva

El código está abierto.

Los dos repositorios son públicos: la API en C# y el cliente en Next.js. Nada de lo que se afirma en esta página necesita que me creas.

Ver todos los proyectos