Ir al contenido

Plataforma de Documentación — Arquitectura técnica (Capa B de APP-000)

Repo: 000-dev-ops-model en Gitea self-hosted (git.prismgrp.com), con Gitea Actions Requirement (el qué): Digital Transformation Director Decisiones técnicas (el cómo): Product Engineering Lead — marcadas [Decisión del Lead]


Qué es este documento y qué no. Es la Capa B de esta app: cómo está construida la plataforma de documentación. Tres delimitaciones, para que no se solape con nada:

  • La doctrina no está aquí. Por qué existen tres tiers, qué es cada capa de conocimiento, por qué nada se duplica y por qué las vistas se generan: sección 06 · Arquitectura de Información. Este documento asume esas reglas y describe su implementación física en esta app.
  • El trabajo pendiente no está aquí. El checklist de montaje, el andamiaje de los 16 repos y la migración de fuentes viven en el plan de la tarjeta: plans/20260903_APP-000-0808_plataforma-documentacion.md.
  • El código no se copia aquí. Este documento describe el contrato de cada pieza y apunta al archivo real. Una copia del código en un .md se desactualiza en el primer ajuste (antipatrón A6).

Generador: Starlight, decidido el 2026-09-03 — registro y razonamiento.


Consolidar la documentación hoy dispersa (SharePoint, READMEs por repo, artefactos sueltos, decisiones del board) en una plataforma que sirve a tres consumidores desde una sola fuente de markdown en Git:

  1. Equipo interno — el modelo operativo (secciones 00–11) y los docs técnicos por app. Un programador nuevo se forma leyendo esto.
  2. Usuarios externos — help desk y tutoriales, un sitio por portal (Customer-Hub, vendor-portal).
  3. Agentes de IA — los mismos docs como contexto, mantenidos frescos por los agentes de documentación (⑦ Publisher, ⑩ Knowledge Curator).

Cuatro ideas, y con ellas se entiende el resto:

  • Cada doc se escribe en un solo lugar: su app. La documentación de cada app vive como markdown dentro de ese repo, en docs/. Esa carpeta es la única fuente de verdad — el único sitio donde alguien edita.
  • content/ es una copia generada, no una segunda fuente. El sync junta los docs/ de todas las apps y los copia a content/. El contenido queda momentáneamente en dos lugares, pero content/ es un artefacto de build desechable: está en .gitignore, nadie lo edita a mano y se regenera desde cero en cada corrida. Por eso no puede desincronizarse. Es la relación entre un documento fuente y el PDF que exportas de él.
  • sites/ es el motor; content/ son las páginas. Starlight es la imprenta —tema, navegación, búsqueda—; content/ es el texto que se imprime. La imprenta se arma una vez; el texto se ensambla solo en cada cambio.
  • El flujo completo: la app escribe en su docs/ → el sync copia a content/ → Starlight renderiza content/ → se publica el sitio.
000-dev-ops-model/
docs/
internal/
modelo-operativo/ # las secciones 00-11: el primer módulo (Capa A)
plataforma-documentacion/ # este archivo: la Capa B de esta app
decisions/ # decisiones fechadas de este repo (estilo ADR)
agent/ # referencia densa solo para agentes
plans/ # los planes; su URL es la que apunta la tarjeta de SeaTable
sync/
sources.yml # qué repos, qué rama, qué audiencias existen
sync-docs.mjs # agregación multi-repo -> content/
stage-content.mjs # monta el tier de content/ dentro de cada sitio (ver §7)
scripts/
lint-docs.mjs # gate de front-matter y de tier (compartido con los repos de app)
check-no-leak.mjs # gate de no-fuga entre audiencias
sites/
shared/ # lo común a los sitios: design tokens y plugins de Markdown
internal/ # sitio interno (Starlight)
src/landing/index.md # su portada, VERSIONADA aquí (ver §5)
external/ # portal de clientes (un proyecto por audiencia)
src/landing/index.md
skills/ # configuraciones de agentes (vacía: ninguno activado aún)
templates/ # plantillas de artefactos del modelo (vacía)
content/ # GENERADO, gitignored
internal/000-dev-ops-model/ # el docs/internal de este repo, con sus módulos
internal/<app>/ # todas las apps -> un solo sitio interno
<audiencia>/<app>/<iface>/ # por audiencia -> portal externo
agent/<app>/ # por app -> corpus de agentes
agent/index.json # manifiesto del corpus
routing.json # procedencia de cada archivo copiado (habilita el gate de no-fuga)
package.json # solo dependencias del sync (yaml, gray-matter)
.releaserc.json # versionado semántico, delegado en ci-actions
.gitea/workflows/
deploy.yaml # sync + gates + build + publicación + despliegue
docs-lint.yml # gate de PR sobre el docs/ propio

No hay segmento de idioma. La documentación de Prism se escribe y se publica en español —lo que va en inglés son los identificadores del sistema: comandos, columnas, campos, rutas, nombres de agentes (00 · Índice Maestro, «Reglas permanentes»)—, así que cada sitio declara un único locale y las páginas salen sin prefijo. Un docs/internal/es/ en un repo de app detiene el sync, con el mensaje de qué hacer: es un árbol escrito contra la estructura anterior, y copiarlo tal cual dejaría una sección llamada es dentro del portal. El gate docs-lint lo dice antes, en el PR del repo que lo escribió.

Este repo es a la vez fuente y sitio. No hay carpeta de contenido con otro nombre: las secciones del modelo son contenido navegable y viven en docs/internal/, como en cualquier app. El sync no es redundante aunque fuente y sitio coincidan: su trabajo real es la agregación multi-repo —juntar el docs/ propio con el de las otras 16 apps en un solo content/—. Para este repo lee local; para las demás, remoto; es el mismo paso.

Starlight no se instala en la raíz: cada proyecto de sites/ tiene su propio package.json. El package.json de la raíz existe solo para el sync y los gates.

<app-repo>/
docs/
public/ # -> help desk externo (solo repos con usuario final)
internal/ # -> sitio interno
agent/ # -> solo agentes
_meta.yml # id, display name, order, app_id
plans/
.gitea/workflows/
docs-lint.yml # gate de PR
notify-docs.yml # dispara el rebuild del sitio al mergear docs/

Un repo de app suma una carpeta y dos workflows. Nada más: sin generador, sin dependencias, sin build. docs/ va en la raíz, nunca dentro de backend/, frontend/ o client/.

Quién lleva public/: los seis repos con usuario final que documentar. Su contenido no espera a que se le asigne un portal: cada interfaz declara su audiencia en _meta.yml y el sync la escribe en el árbol de esa audiencia. Lo que falta hoy es el sitio de dos de ellas (§7), no el ruteo. Los diez repos restantes llevan docs/{internal,agent}. El mapa completo, con la regla de asignación, está en la página 06a del modelo.

El eje que decide el destino humano es la audiencia; la doctrina está en 06 §6.1. Aquí, su implementación física:

Origen Destino Corpus de agentes
docs/internal/** content/internal/<app>/ si agent: true
docs/public/<iface>/** content/<audiencia>/<app>/<iface>/, una copia por cada audiencia que declare si agent: true
docs/agent/** ✅ siempre

Bajo public/ va una carpeta por interfaz, con el nombre que esa interfaz tiene en el código. El sync rechaza un nombre de locale ahí (public/es/) por partida doble: no tendría a qué mapearse en el _meta.yml —la audiencia se declara por interfaz— y la plataforma publica en un solo idioma.

La audiencia de cada interfaz sale del docs/_meta.yml de la app, y puede ser una o varias:

audiences:
portal: vendors
admin: collaborators
common: [collaborators, customers]

La lista cerrada de audiencias vive en sync/sources.yml, en un solo sitio, y el workflow se la pasa al linter para no tenerla escrita dos veces.

Front-matter obligatorio en todo .md de la Base: title, owner, last_verified, sources, version. Opcionales: agent (booleano) y sidebar_position.

Lo que el gate de docs hace cumplir (scripts/lint-docs.mjs, en cada PR de cada repo): un archivo sin tier no entra; un .md sin los cinco campos no entra; una interfaz bajo public/ sin audiencia declarada detiene el build, y una audiencia fuera de la lista cerrada también. Tampoco pasa un _meta.yml que declare una interfaz inexistente: un mapa mentiroso es peor que uno ausente. Ni una carpeta de idioma (es/, en/) dentro de un tier: la plataforma publica en un solo idioma y sin prefijo de locale.

No-fuga, verificada y no solo prometida. Un árbol de audiencia se escribe únicamente desde docs/public/<iface>/, y solo hacia las audiencias que su _meta.yml declara. Como una garantía que nadie comprueba es solo una promesa, el sync registra en content/routing.json la procedencia y la audiencia de cada archivo, y scripts/check-no-leak.mjs lo verifica en tres direcciones:

  1. Nada con origen internal/ o agent/ aparece en un portal de audiencia — la fuga clásica: documentación del equipo en el portal de un cliente.
  2. Ningún archivo aparece en una audiencia distinta de la que se le registró — la fuga que abre este modelo: un documento de proveedores en el portal de clientes.
  3. Nada existe en un portal sin estar registrado — detecta lo escrito fuera del sync.

sync/sources.yml — agregar una app a la plataforma es agregar una entrada (lo hace el botón Create Repo de Applications). Cada entrada declara id, repo (owner/name exacto en Gitea), ref y docs_path. No hay campo de idioma: todo el corpus se publica en español. El repo central se declara aparte, como fuente local (central.path).

El nombre del repo se lee de Gitea, no de SeaTable. El campo Git URL de la tabla Applications quedó rezagado del renombrado a app-NNN-* y algunas filas aún apuntan a GitHub. sources.yml lleva los nombres verificados contra la API de Gitea (2026-09-03).

sync/sync-docs.mjs — su contrato:

  • Hace sparse-checkout de solo docs_path por repo, sin blobs del resto: el código de la app nunca llega al build.
  • Rutea a content/ según la tabla de §4 y materializa el corpus de agentes.
  • Escribe content/agent/index.json (el manifiesto del corpus) y content/routing.json (la procedencia de cada archivo).
  • Regenera content/ desde cero en cada corrida.
  • Falla ruidosamente: si un repo no se puede leer, el proceso termina con error y el build no continúa. Un sitio publicado al que le falta una app sin avisar es peor que un build roto. Un repo que existe pero todavía no tiene docs/ no es un error: se reporta como “falta andamiaje” y el build sigue.
  • Modo --local-only: corre solo con el repo central, sin token y sin tocar los remotos. Es la prueba local de punta a punta (npm run sync:local).

Las portadas no se generan: se commitean. La home de cada portal vive dentro de su propio sitio, en sites/<portal>/src/landing/index.md, versionada en git junto a src/components/ y src/styles/. No es una fuente del corpus: no se declara en sources.yml, no pasa por sync-docs.mjs y no se registra en routing.json. Quien la monta es stage-content.mjs, que la copia en la raíz de src/content/docs/ al final, después del corpus, para que ninguna app pueda pisar la home de un portal dejando un index.md en la raíz de su tier.

Los dos árboles por los que pasa —content/ y src/content/docs/— son espejos desechables que se borran enteros en cada corrida, así que la portada no puede vivir en ninguno de los dos: en content/ desaparecería en el primer build limpio de CI, y en src/content/docs/ la borraría el rmSync de stage-content.mjs. src/landing/ es el directorio hermano que sí se versiona. Si a un sitio le falta, stage-content.mjs falla en seco: sin portada la raíz del portal da 404, y es la única página que el sitio tiene garantizada.

Consecuencia a tener presente: content/<audiencia>/ puede quedar legítimamente vacío mientras ninguna app publique para esa audiencia — hoy es el caso de customers. Antes la portada lo rellenaba y enmascaraba esa condición; por eso el guard de corpus vacío en deploy.yaml solo exige content/internal.

Procedencia de esta decisión. Ni landing/ ni esta sección vienen del diseño original: la v2 de este documento no dice nada sobre de dónde sale la home de un portal. Es una decisión de implementación tomada al montar Starlight, primero como un landing/ en la raíz del repo y luego movida dentro de cada sitio para no ensuciar el root.

Todo en .gitea/workflows/. La sintaxis de Gitea Actions es en gran medida compatible con la de GitHub Actions, pero en esta instancia no se puede usar ni una acción — resuelto contra la instancia real, ya no es decisión abierta:

La instancia tiene REQUIRE_SIGNIN_VIEW y el runner descarga las acciones con go-git, sin credenciales, antes de que empiece ningún step. Ni actions/checkout, ni actions/setup-node, ni actions/upload-artifact llegan a resolverse. Todo va con run:: el checkout se hace a mano con http.extraHeader, y el transporte de artefactos entre runners usa el registro de paquetes GENERIC de la propia instancia. Los workflows reutilizables sí funcionan (Cross-Repository Access).

Dos trampas de sintaxis de esta instancia: un bloque concurrency: o un type: en los inputs hacen que descarte el workflow entero en silencio —sin job, sin error y sin rastro en la UI—. Y Gitea recorre los directorios de workflows y corta en el primero que existe: .gitea/ y .github/ no se suman, así que todo vive en .gitea/workflows/.

Workflow Dónde Qué hace
docs-lint.yml repo central y cada repo de app Gate de PR sobre docs/: tier + los cinco campos de front-matter
notify-docs.yml cada repo de app Al mergear a main un cambio en docs/, dispara el rebuild del sitio
deploy.yaml repo central Lint → sync → gate de no-fuga → build de los dos sitios → publicación del artefacto → despliegue

deploy.yaml corre en tres máquinas. El build va en el runner grande (sea-debian-x86-64); los dist viajan al host de despliegue por el registro de paquetes, versionados por SHA; el despliegue lo hace el runner del entorno —stag-debian-x86-64 desde staging, prod-debian-x86-64 desde main—, que además pide el versionado a ci-actions.

El gate de los repos de app no se copia 16 veces. docs-lint.yml baja scripts/lint-docs.mjs del repo central en cada corrida: la regla vive en un solo sitio y cambiarla no exige tocar 16 repos.

El andamiaje de un repo nuevo no vive en este repo. Lo siembra el script de SeaTable detrás del botón Create Repo de la tabla Applications: crea el repo, sus carpetas de tier, su _meta.yml, sus dos workflows y su entrada en sources.yml. Es automatización de SeaTable, no tooling del repo central.

Disparo cross-repo. Lo escrito hoy usa la API de dispatch de Gitea contra el workflow del sitio, pasando el repo de origen como input informativo. La alternativa es un webhook del repo de app al del sitio. [Decisión del Lead]

Nada sale en verde sin haber publicado algo. Un build que no falla pero deja un portal inservible es el modo de fallo que este workflow persigue, y por eso comprueba a mano lo que Astro no comprueba: que el sync exigió su credencial en lugar de degradar a un corpus parcial; que cada tier tiene al menos un .md tras el sync (Starlight genera felizmente un sitio de cero páginas); que cada dist/ trae su index.html y su índice de Pagefind —sin él el portal se publica con el buscador roto y nadie se entera hasta usarlo—; y que SITE_PATH_PREFIX está bien formado, porque un base que no coincide con la ruta pública produce un sitio sin CSS ni JS con el build en verde.

Un proyecto por sitio bajo sites/, cada uno con su package.json, su astro.config.mjs y su src/content.config.ts.

Cuántos sitios: cuatro, y no crecen. Uno por audiencia — equipo, colaboradores, proveedores, clientes (06 §6.1). Cada app aporta su sección dentro del portal que le corresponde, así que agregar apps no agrega sitios: es la propiedad que hace sostenible el montaje. Cada portal lee su porción del árbol generado:

Proyecto Lee Audiencia
sites/internal content/internal/ Equipo
sites/external content/customers/ Clientes
— pendiente — content/collaborators/ Colaboradores
— pendiente — content/vendors/ Proveedores

Los dos portales pendientes ya se generan en content/: les falta el proyecto, no el contenido. Abrir uno es clonar sites/external y agregar su tier a TARGETS en stage-content.mjs.

El cableado del contenido es la pieza no obvia, y no salió como se había previsto. Starlight no soporta apuntar su colección docs a una carpeta fuera del proyecto: su navigation.ts resuelve autogenerate con un replace de src/content/docs/ sobre el filePath, y absolutePathToLang.ts detecta el idioma del mismo modo. Con una base externa —docsLoader({ base: '../../content/internal' })ambos fallan en silencio: sidebar vacío y etiquetas en el idioma equivocado (starlight#1257).

Por eso hay un paso intermedio, sync/stage-content.mjs, que monta el tier correspondiente dentro de cada sitio, en src/content/docs. Corre como prebuild/predev de cada proyecto, así que en local no hay que llamarlo a mano. El contrato que preserva es el mismo que se buscaba: el contenido tiene una sola fuente, content/, y el árbol montado es otro artefacto desechable — ambos están en .gitignore.

Al montar es donde el corpus transversal deja de ser “una fuente más”: el docs/ del repo central sube a la raíz de la colección (modelo-operativo/, plataforma-documentacion/, decisions/) y el resto de las fuentes cae bajo apps/<id>/. Eso es lo que hace direccionables los grupos del sidebar sin enumerar cada app.

Orden completo en CI: lint-docssync-docscheck-no-leakstage-contentastro build.

Búsqueda: Pagefind, que Starlight trae por defecto — indexa en el build y corre del lado del cliente, que es lo que la hace funcionar detrás del login.

Identidad visual y sites/shared/. Los design tokens son uno solo para los dos sitios: sites/shared/design-tokens/prism.css, con src/styles/prism.css de cada sitio reservado para sus propios overrides. Junto a ellos vive sites/shared/callouts.mjs, un plugin de Markdown que convierte las citas con marcador (> [!NOTE]) en asides de Starlight. Tres componentes se sustituyen porque los originales no dan lo que el portal necesita: PageTitle (breadcrumbs, que Starlight no trae), ThemeSelect (el original usa un <select> nativo, cuyo popup lo pinta el sistema operativo y no se puede integrar con el tema) y Pagination. LanguageSelect tenía su propia versión mientras el sitio fue bilingüe; con un solo locale Starlight ya no lo pinta y el override se retiró.

Consecuencia operativa: como ambos astro.config.mjs importan de ../shared/, los sitios se construyen desde la raíz del repo (npm --prefix sites/<sitio> run build). Un build aislado dentro de sites/<sitio>/ no resuelve esas rutas.

Idioma: uno, y sin prefijo en la URL. Ambos sitios declaran un único locale, el root de Starlight (root: { label: 'Español', lang: 'es' }): las páginas salen en /<base>/modelo-operativo/… y la portada, en /<base>/ misma. lang: 'es' sigue haciendo falta — es lo que pone el lang del <html>, elige las cadenas de UI de Starlight y nombra el archivo de traducciones (src/content/i18n/es.json). Con un solo locale Starlight no pinta el selector de idioma.

Ninguno de los dos sitios tiene ya redirects: la portada se genera en la raíz del portal, así que no hay a dónde redirigir. Mientras hubo /es/ y /en/ sí lo había, porque la raíz no generaba ninguna página.

Dos credenciales, ambas con su forma exacta pendiente según cómo Gitea emita tokens en la instancia. [Decisión del Lead]

Credencial Función Alcance necesario
DOCS_SYNC_TOKEN El sync lee el docs/ de los repos de apps; el docs-lint de cada app baja el linter Lectura en los repos de apps
DOCS_DISPATCH_TOKEN El repo de app dispara el rebuild del sitio Disparar ese workflow, nada más

Notas de diseño, a validar contra la instancia:

  • La credencial de lectura es un token de acceso de Gitea (cuenta de servicio). No hay equivalente de “GitHub App”: hay que evaluar su vida útil y su rotación.
  • Se conserva el mínimo privilegio: lectura donde solo se lee; disparo acotado al repo del sitio. Ningún token con más alcance del necesario.
  • Autenticación: la sesión se hereda, no se resuelve otra vez. Los sitios son estáticos, así que la autenticación no vive en el generador sino en el reverse proxy de adelante, contra Core Identity. Al portal del equipo se entra directo. A los tres portales de usuario se entra desde dentro de la app, por su botón de help desk: el usuario ya traía sesión, así que los portales no montan login propio (06 §6.2).
  • Hoy los dos sitios son PÚBLICOS. La autenticación del sitio interno está en stand-by, pospuesta a propósito: es un ajuste del Caddyfile del servidor, no de este repo. El workflow ya lo contempla — la variable de repo EXPECT_INTERNAL_AUTH=true hace que el paso de humo vuelva a exigir 401/403 en /internal sin cookie—, así que el día que se implemente el gate lo verifica en cada despliegue en vez de creerse la config.
  • Qué queda abierto: una sola regla. La documentación de acceso —propiedad del repo de Core Identity, copiada por el sync a los tres portales— es la sección abierta de cada uno; todo lo demás exige sesión. Tiene que ser así por definición: quien no sabe entrar todavía no tiene sesión para leer cómo entrar.
  • La lista de rutas abiertas es derivable, no se mantiene a mano. content/routing.json registra la procedencia de cada archivo, así que el build puede emitir para el proxy la lista exacta de rutas que provienen de Core Identity. Es la clase de regla que se desactualiza si se escribe a mano. [Decisión del Lead] si se implementa ahora o después.
  • El límite de la protección es la audiencia, no la asignación. Un sitio estático publica el mismo HTML y el mismo índice de búsqueda para todos: filtrar por usuario exigiría una capa dinámica, y un buscador que muestra títulos que no se pueden abrir revela lo que pretende ocultar. Las asignaciones de Core Identity gobiernan el acceso a las apps; la documentación se protege por audiencia (06 §6.2).
  • Secretos: los agentes y workflows transmiten el enlace de 1Password; en ningún caso obtienen, almacenan ni reproducen el secreto.

Los dos sitios son estáticos y los sirve directamente el Caddy de la máquina con file_server, desde <SITE_ROOT>/current/{internal,external}. No hay contenedor propio de este stack: ni Dockerfile, ni docker-compose.yml, ni nginx, ni .env — no queda ningún proceso propio al que configurar.

El dominio depende del entorno, y no se parecen:

Entorno URL Prefijo (base)
Producción wiki.prismgrp.com dominio propio, sin prefijo
Staging staging.prismgrp.com/wiki dominio compartido; Caddy hace handle_path /wiki/*, que recorta el prefijo

Eso obliga a construir con un base distinto por entorno: lo fija el workflow con SITE_PATH_PREFIX y lo hornea Astro en tiempo de build. No es algo ajustable después en el servidor.

Publicación sin ventana de rotura. Cada despliegue crea <SITE_ROOT>/releases/<sha>/ y al final mueve el symlink current de golpe con mv -T, que es un rename(2) atómico: ninguna petición ve un árbol a medio copiar. Descomprimir sobre current habría dejado el portal roto durante segundos en cada deploy, y con un fallo a mitad lo habría dejado roto del todo.

La consecuencia incómoda: el dominio y el ruteo viven en el Caddyfile del servidor, fuera de este repo. La referencia de lo que esos Caddyfiles tienen que decir, uno por entorno, está en hosting-caddy.md — si divergen, gana el servidor y este repo miente. Mantenerlos a la par es trabajo manual.

content/agent/ es un corpus autocontenido —copias materializadas, no punteros— con todo lo que esté bajo docs/agent/** de cada app más cualquier archivo de internal/ o public/ marcado agent: true. content/agent/index.json lista cada archivo con su app y su tier de origen.

Aguas abajo, un paso de ingesta alimenta esto a lo que consuman los agentes: un vector store, un recurso MCP o un bundle plano — [Decisión del Lead]. Como es el mismo corpus que mantienen los agentes de documentación, “lo que leen los agentes” y “lo que se mantiene fresco” son el mismo conjunto.

Hoy el corpus sale vacío: ninguna sección del modelo está marcada agent: true. Si los agentes deben conocer el modelo que los gobierna, hay que marcar las secciones que corresponda — decisión del Director, registrada en el plan.

Las seis decisiones técnicas que desbloquean el montaje están en el bloque A del plan, que es donde se les hace seguimiento: plans/20260903_APP-000-0808_plataforma-documentacion.md.

Las que no son de montaje sino de diseño continuo:

  • Destino de ingesta del corpus de agentes: vector store, recurso MCP o bundle plano.
  • Modelo que ejecuta los agentes de documentación: se evalúa DeepSeek por costo, conservando Cloud como interfaz — coherente con la independencia de herramienta.
  • Autenticación del sitio interno: en stand-by (§8). Cuándo se levanta es decisión del Director; el gate de humo ya está escrito y esperando la variable.
  • Un segundo idioma en los portales de usuario: cerrado por ahora. La política es publicar en español (00 · Índice Maestro), y la plataforma ya no lleva la dimensión de idioma. Si algún día un portal de cliente lo pide, se reabre como decisión de política: técnicamente es agregar el locale en su astro.config.mjs y volver a meter la carpeta de idioma dentro de cada interfaz (docs/public/<iface>/<lang>/).
  • Sincronización local: cómo se configura el clon del Director (sparse, solo docs/) frente al de los devs (completo).
  • Extracción SeaTable → ADR: grado de automatización.

11. Relación con el resto de la documentación

Sección titulada «11. Relación con el resto de la documentación»
Para conocer Dónde
Por qué existen las tres capas y los tres tiers; el status.md; la generación de vistas 06 · Arquitectura de Información
El ciclo del contexto y el Plan 02 §4
Qué agente genera y mantiene cada vista 04
El trabajo pendiente, su checklist y sus criterios de aceptación plans/20260903_APP-000-0808_plataforma-documentacion.md
Por qué Starlight y no Docusaurus ../decisions/20260903_generador-del-sitio.md
Qué tiene que decir el Caddyfile de cada entorno hosting-caddy.md

Versión Fecha Cambio
v3.4 2026-09-08 Sale la dimensión de idioma de toda la plataforma, alineando la Capa B con la política de idioma del modelo (00 · Índice Maestro v3.8: la documentación se publica en español; el inglés es para los identificadores del sistema). §3.1 y §4: el idioma deja de ser el primer segmento de cada tier —content/internal/<app>/, content/<audiencia>/<app>/<iface>/— y una carpeta de idioma dentro de un tier pasa a detener el sync y a fallar el gate docs-lint. §5: sources.yml pierde el campo lang y las portadas viven en sites/<portal>/src/landing/index.md. §7: ambos sitios declaran el locale root de Starlight (URLs sin prefijo), desaparecen los redirects de / y el override de LanguageSelect —quedan tres componentes sustituidos, no cuatro—. §10: la traducción al inglés deja de ser una decisión abierta de contenido y queda como decisión de política, cerrada por ahora.
v3.3 2026-09-08 §5 y §3.1: las portadas salen de landing/ en la raíz del repo y pasan a vivir dentro de su propio sitio, en sites/<portal>/src/landing/<lang>/. Dejan de ser una fuente del corpus: no se declaran en sources.yml, no pasan por sync-docs.mjs y no se registran en routing.json; las monta stage-content.mjs, que falla si a un sitio le falta su src/landing/. Se documenta la consecuencia: content/<audiencia>/ puede estar legítimamente vacío —la portada ya no lo rellena—, así que el guard de corpus vacío de deploy.yaml solo exige content/internal. Se corrige además la redacción de §5, que decía “se versionan” en el sentido de “se commitean” y se leía como versionado de documentación, algo que esta plataforma no hace; y se anota la procedencia real de la decisión, ausente del diseño original.
v3.2 2026-09-04 Se cierran contra la implementación real las tres cosas que v3.1 daba por abiertas o por supuestas. §6: en esta instancia no se puede usar ninguna acción (REQUIRE_SIGNIN_VIEW + go-git sin credenciales); todo va con run:, los artefactos viajan por el registro de paquetes y build-docs.yml queda sustituido por deploy.yaml. §7: Starlight NO admite una colección fuera del proyecto —falla en silencio, no con error—, así que entra stage-content.mjs como paso de montaje; se documentan sites/shared/, los componentes sustituidos y los dos idiomas. §8.1: hosting resuelto (Caddy file_server, releases por SHA con symlink atómico, base por entorno), con la autenticación del sitio interno explícitamente en stand-by. §3.1 y §4: el idioma pasa a ser el primer segmento de cada tier, y landing/ versiona las portadas.
v3.1 2026-09-03 Ruteo reimplementado sobre el eje de audiencia (§4): destino por docs/public/<iface>/ con el mapa del _meta.yml, lista cerrada en sources.yml, y el gate de no-fuga verificando ahora también la fuga entre audiencias. §7: cuatro portales fijos que no crecen con el parque de apps. §8: la sesión se hereda desde la app, la documentación de acceso de Core Identity es la única sección abierta, y el límite de protección es la audiencia — con el porqué técnico.
v3.0 2026-09-03 Reenfocado como Tech Spec puro de la Capa B. Sale el material de plan que estaba mezclado (checklist de implementación, migración de fuentes, decisiones abiertas de montaje) → al plan de la tarjeta APP-000-0808. Sale el cuadro de decisión de herramienta → decisions/20260903_generador-del-sitio.md. Sale la doctrina que duplicaba a la sección 06 (definición de tiers y ejes), que ahora se referencia. Salen las copias del código de sync-docs.mjs y lint-docs.mjs: se describe su contrato y se apunta al archivo real, que ya existe. Se documentan las piezas nuevas: routing.json y el gate de no-fuga, el linter compartido con los repos de app, el cableado del contenido en Starlight y el deploy que avisa en vez de fingir. Renumeración completa de secciones.
v2.0 2026-09-01 Requirement + esqueleto de Tech Spec para la conversación técnica con el Lead; entorno Gitea; decisiones marcadas [Decisión del Lead]. Actualizado el 2026-09-03 con Starlight y Core Identity.