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
.mdse desactualiza en el primer ajuste (antipatrón A6).Generador: Starlight, decidido el 2026-09-03 — registro y razonamiento.
1. Objetivo
Sección titulada «1. Objetivo»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:
- Equipo interno — el modelo operativo (secciones 00–11) y los docs técnicos por app. Un programador nuevo se forma leyendo esto.
- Usuarios externos — help desk y tutoriales, un sitio por portal (Customer-Hub, vendor-portal).
- Agentes de IA — los mismos docs como contexto, mantenidos frescos por los agentes de documentación (⑦ Publisher, ⑩ Knowledge Curator).
2. Modelo mental para quien implementa
Sección titulada «2. Modelo mental para quien implementa»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 losdocs/de todas las apps y los copia acontent/. El contenido queda momentáneamente en dos lugares, perocontent/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 acontent/→ Starlight renderizacontent/→ se publica el sitio.
3. Estructura de repositorios
Sección titulada «3. Estructura de repositorios»3.1 Repo central — 000-dev-ops-model
Sección titulada «3.1 Repo central — 000-dev-ops-model»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/ propioNo 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.
3.2 Cada repo de app
Sección titulada «3.2 Cada repo de app»<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.
4. Ruteo de contenido
Sección titulada «4. Ruteo de contenido»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:
- Nada con origen
internal/oagent/aparece en un portal de audiencia — la fuga clásica: documentación del equipo en el portal de un cliente. - 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.
- Nada existe en un portal sin estar registrado — detecta lo escrito fuera del sync.
5. Config de fuentes y el sync
Sección titulada «5. Config de fuentes y el 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 URLde la tabla Applications quedó rezagado del renombrado aapp-NNN-*y algunas filas aún apuntan a GitHub.sources.ymllleva los nombres verificados contra la API de Gitea (2026-09-03).
sync/sync-docs.mjs — su contrato:
- Hace sparse-checkout de solo
docs_pathpor 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) ycontent/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 unlanding/en la raíz del repo y luego movida dentro de cada sitio para no ensuciar el root.
6. CI / workflows
Sección titulada «6. CI / workflows»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_VIEWy el runner descarga las acciones con go-git, sin credenciales, antes de que empiece ningún step. Niactions/checkout, niactions/setup-node, niactions/upload-artifactllegan a resolverse. Todo va conrun:: el checkout se hace a mano conhttp.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 untype: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.
7. Los sitios Starlight
Sección titulada «7. Los sitios Starlight»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-docs → sync-docs → check-no-leak → stage-content →
astro 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.
8. Credenciales, autenticación y hosting
Sección titulada «8. Credenciales, autenticación y hosting»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=truehace que el paso de humo vuelva a exigir 401/403 en/internalsin 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.jsonregistra 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.
8.1 Hosting — resuelto
Sección titulada «8.1 Hosting — resuelto»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.
9. Corpus de agentes
Sección titulada «9. Corpus de agentes»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.
10. Decisiones abiertas del Tech Spec
Sección titulada «10. Decisiones abiertas del Tech Spec»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.mjsy 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 |
Historial de versiones
Sección titulada «Historial de versiones»| 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. |