Saltar al contenido principal

Mantener la documentación

Toda documentación se pudre. La pregunta no es si va a desactualizarse, sino cuánto trabajo cuesta que no lo haga. Aquí la respuesta son cuatro capas, de la más automática a la que depende de una persona.

1. Generar en vez de escribir​

Lo que se genera no se desactualiza. Cinco generadores, en docs/scripts/, leen el código y escriben JSON en docs/generated/:

GeneradorLeeProduce
gen-endpoints.mjsDecoradores de los *.controller.tsLa referencia de API
gen-env.mjs.env.exampleLas variables de entorno
gen-rutas.mjsfrontend/src/app/**/page.tsxEl inventario de pantallas
gen-tipos.mjsshared/src/**/*.tsLos tipos compartidos
gen-migraciones.mjssupabase/migrations/*.sqlEl historial de migraciones

Los endpoints se leen con el AST de TypeScript, no arrancando Nest: no hace falta Redis, ni Supabase, ni variables de entorno, así que funciona igual en cualquier portátil y en el CI. Del mismo análisis sale openapi.json, que se escribe en docs/static-interno/ —una carpeta que solo se publica en este sitio, no en el manual público.

Se regeneran con:

cd docs && yarn gen

Los JSON se versionan a propósito. Así el diff de un PR enseña que la superficie del producto cambió, que es justo lo que hace que un revisor pregunte «¿y esto no habría que documentarlo?».

2. Que el CI falle ante la deriva​

El job docs del CI hace tres cosas:

  • Construye los dos sitios con onBrokenLinks: 'throw'. Un enlace muerto rompe el build.
  • Regenera y compara. Si docs/generated/ o docs/static-interno/ cambian al regenerar, el CI se pone rojo: alguien añadió un endpoint y no ejecutó yarn gen.
  • Comprueba la cobertura con yarn coverage: cada módulo del backend, cada sección del panel y cada canal tiene que estar mencionado en alguna página. Las excepciones deliberadas van en docs/.docsignore, con el motivo escrito al lado.

Un cuarto job, commits, valida los mensajes del PR con scripts/validar-commit.mjs, que es el mismo que corre el hook local.

También fallan los componentes: si escribes <Endpoints modulo="pagos" /> y ese módulo desaparece del backend, el componente lanza un error durante el build en lugar de renderizar una tabla vacía.

3. Lo que se espera de ti​

Documenta en el mismo commit que la funcionalidad. No en el siguiente, no «cuando esté estable». La documentación escrita una semana después la escribe alguien que ya no recuerda por qué tomó esa decisión.

Concretamente:

Si cambiasHaz también
Un endpointComenta el método con JSDoc: eso es su descripción en la referencia
Una variable de entornoComéntala encima en .env.example
Una pantalla del panelActualiza la página del manual de usuario que la describe
Una tool del botActualiza Bots y LangGraph y el manual de usuario
El esquemaPrimera línea de la migración como comentario -- explicando el cambio

La plantilla de PR tiene una casilla de documentación, y el workflow docs-reminder.yml comenta automáticamente cuando un PR toca controladores, páginas, canales, tools, migraciones o tipos sin tocar docs/. Ese aviso no bloquea: solo hace difícil olvidarse.

Y como buena parte del desarrollo aquí es asistido por IA, la instrucción también está en CLAUDE.md, con la tabla de qué actualizar según lo que toques. En la práctica es la palanca más efectiva de las cuatro.

4. Notas de versión​

Los commits siguen Conventional Commits (feat(health):, fix(whatsapp):) y eso lo comprueban dos cosas: el hook commit-msg de .githooks/, que se activa solo al ejecutar npm install en la raíz, y el job commits del CI, que no se puede saltar con --no-verify.

Los dos ejecutan scripts/validar-commit.mjs, unas cuantas líneas de expresión regular en vez de commitlint: son los mismos cien paquetes de dependencia para lo mismo, y así el mensaje de error puede enumerar los tipos válidos en castellano.

Con los mensajes en formato, git-cliff (configurado en cliff.toml) genera el post de docs/blog/ agrupando por tipo y etiquetando cada línea con su ámbito traducido — health sale como «Historia clínica», whatsapp como «WhatsApp». Lo hace promote.yml al cortar una versión. Commitea el post en main con lo fusionado desde la última etiqueta, crea la etiqueta y lanza el sync que lleva el post a dev.

Para ver cómo quedarían las notas antes de promover, con git-cliff instalado:

git cliff --unreleased --tag v0.2.0

5. Que los sitios sigan abriendo (test y prod)​

Actualizada no basta: tiene que ser alcanzable. Los servicios docs y devdocs viven en el mismo docker-compose.yml de ambos ambientes (puertos locales 3840 / 3841).

TestProducción
Manualdocstest.xenpia.comdocs.xenpia.com
Técnicodevdocstest.xenpia.comdevdocs.xenpia.com
  • DNS: *.xenpia.com cubre prod; test necesita registros explícitos a .50.
  • Cloudflare Access solo en devdocs* / devdocstest* (nunca en el wildcard entero).
  • Si el contenedor responde en 127.0.0.1:3840 y el dominio no, falta nginx/DNS en ese servidor.

Detalle y bloques de nginx: Despliegue §5.b. Runbooks si deja de abrir: Runbooks.

6. Instrucciones para agentes (Claude y Cursor)​

Para que la IA no se salte este proceso:

ArtefactoQuién lo lee
Skill docs-as-codeClaude Code (.claude/skills/), Cursor (.cursor/skills/) y .agents/skills/
Regla .cursor/rules/documentacion.mdcCursor en todas las conversaciones (alwaysApply)
CLAUDE.mdClaude Code y Cursor (regla de workspace)

La fuente canónica del skill es .claude/skills/docs-as-code/. Tras editarlo, cópialo a .cursor/skills/ y .agents/skills/ (ver .cursor/skills/README.md).

Escribir aquí​

  • Contenido de usuario en docs/manual/, técnico en docs/interno/.
  • .md para texto normal, .mdx solo si necesitas componentes.
  • Los componentes <Endpoints>, <EnvVars>, <Pantallas>, <Tipos>, <Migraciones> y <Resumen> están disponibles en cualquier .mdx sin importarlos.
  • Una página nueva aparece sola en el menú: los sidebars se generan desde la estructura de carpetas. El orden se controla con los _category_.json.
  • Si una descripción del frontmatter lleva dos puntos, ponla entre comillas o el YAML se rompe.
  • Los anclajes con tilde no coinciden con su versión sin tilde. Si vas a enlazar a un apartado, dale un identificador explícito: ## Cómo agenda el bot {#como-agenda-el-bot}.

Y sobre todo​

Escribe lo que no se puede deducir del código: por qué se tomó una decisión, qué se intentó antes, qué se rompe si tocas esto. Lo que sí se deduce del código, genéralo.