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/:
| Generador | Lee | Produce |
|---|---|---|
gen-endpoints.mjs | Decoradores de los *.controller.ts | La referencia de API |
gen-env.mjs | .env.example | Las variables de entorno |
gen-rutas.mjs | frontend/src/app/**/page.tsx | El inventario de pantallas |
gen-tipos.mjs | shared/src/**/*.ts | Los tipos compartidos |
gen-migraciones.mjs | supabase/migrations/*.sql | El 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/odocs/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 endocs/.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 cambias | Haz también |
|---|---|
| Un endpoint | Comenta el método con JSDoc: eso es su descripción en la referencia |
| Una variable de entorno | Coméntala encima en .env.example |
| Una pantalla del panel | Actualiza la página del manual de usuario que la describe |
| Una tool del bot | Actualiza Bots y LangGraph y el manual de usuario |
| El esquema | Primera 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).
| Test | Producción | |
|---|---|---|
| Manual | docstest.xenpia.com | docs.xenpia.com |
| Técnico | devdocstest.xenpia.com | devdocs.xenpia.com |
- DNS:
*.xenpia.comcubre 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:3840y 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:
| Artefacto | Quién lo lee |
|---|---|
Skill docs-as-code | Claude Code (.claude/skills/), Cursor (.cursor/skills/) y .agents/skills/ |
Regla .cursor/rules/documentacion.mdc | Cursor en todas las conversaciones (alwaysApply) |
CLAUDE.md | Claude 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 endocs/interno/. .mdpara texto normal,.mdxsolo si necesitas componentes.- Los componentes
<Endpoints>,<EnvVars>,<Pantallas>,<Tipos>,<Migraciones>y<Resumen>están disponibles en cualquier.mdxsin 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.