dec-0124

Vista generada de dec-0124: Documentación como contrato: Fumadocs, Diátaxis, frontmatter de contrato, referencia generada y guards que impiden posponerla

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0124-docs-como-contrato.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0124-docs-como-contrato.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.

CampoValor
EstadoPROPOSED
Fecha2026-10-01
Ficherodocs/decisions/dec-0124-docs-como-contrato.md

Por qué importa (del frontmatter del ADR):

Fija que la documentación de producto de Styx es un contrato atado a styx.model.yml y a los ADRs: dónde vive (apps/docs/content, convenciones Fumadocs), su arquitectura de información (Diátaxis + producto + operación + contribución + roadmap/ADRs), el frontmatter de contrato (nodos, gates, ADRs, estado especificado|implementado|verificado), qué se genera y desde qué fuente (OpenAPI, TSDoc, Zig ///, BUS_ROUTES, protocols, CLI, config) y los guards check:docs-contract y check:headless-parity, incluido que un gate item no pasa sin su docu. Sin este ADR cada lane documentaría a su manera, la referencia se escribiría a mano y se desviaría del código, y la docu se pospondría al final (lo que waxin prohíbe).

Nodos del roadmap que lo citan en refs: ninguno.

Páginas de la documentación que lo citan: Ciclo de vida (especificado), Generadores de la documentación (especificado), Escribir una página de contrato (especificado), Releases (especificado), Dispositivos, mando y handoff (especificado), Servidores conectados y Jellyfin (especificado), Metadatos en el fichero (especificado), Modelo de datos (especificado), Modo público y Styx como plataforma (especificado), Transferencias (especificado), API para agentes y MCP (especificado), Importar NFO de Jellyfin (especificado), Activar los metadatos incrustados (especificado), Raíces y fuentes (especificado), Storage Box (especificado), Subir ficheros (especificado), Comandos del CLI (especificado), Salida JSON y códigos de salida (especificado), API keys (especificado), Conectar la TV y el CLI (especificado), Hogares y perfiles (especificado), Iniciar sesión (especificado), Guías (especificado), Motores Zig y libav (especificado), Navegadores (especificado), Pistas y subtítulos (especificado), Styx (especificado), Actualizar y versiones (especificado), Desplegar la documentación (especificado), Instalar (especificado), Visión (especificado), Primer arranque (especificado), Primera biblioteca (especificado), Primera película en Chrome (especificado)

Texto del ADR

Leído de docs/decisions/dec-0124-docs-como-contrato.md, el fichero canónico.

dec-0124 — Documentación como contrato: Fumadocs, Diátaxis, frontmatter de contrato, referencia generada y guards que impiden posponerla

  • Fecha: 2026-10-01
  • Estado: PROPOSED. No es un lock. waxin lo lockea vía AskUserQuestion, con las preguntas de §10 resueltas o diferidas de forma explícita. Mientras no se lockee, avanza sólo lo aditivo y reversible: el árbol de contenido, los generadores en modo --check y los guards en modo informe (tickets 01–04 de docs/track/docs/tickets/). Ningún guard bloquea CI hasta el lock.
  • Decisión parcial de waxin (2026-10-02, vía AskUserQuestion): P1 resuelta. El sitio Fumadocs va sobre TanStack Start (el mismo stack que apps/web). La plantilla de Fumadocs se elige después con /prototype (variantes renderizadas en el workbench, lock de waxin del 2026-07-11 para decisiones visuales), no por AskUserQuestion. El ADR sigue PROPOSED: P2–P6 de §10 siguen abiertas.
  • Propone: lane docs (rama w9/docs). Nodo track/docs, alta propuesta en styx.model.yml. Plan en docs/track/docs/plans/00-plan.md.
  • Dirección de waxin que cumple (2026-10-01):
    1. D1: documentación completa que funcione como contrato que obliga a cumplir, atada al roadmap y a los ADRs. Describe el sistema objetivo, no sólo el de hoy. Los guards impiden atajos y que la docu se deje para el final. Sitio futuro en Fumadocs, con la plantilla aún sin elegir y el MDX "chulo" para después. Ahora toca el contenido MD/MDX con las convenciones de Fumadocs, y generar todo lo que se pueda generar.
    2. D2: headless first-class. Todo lo que ofrece la UI existe por API (core → SDK → consumidores) y por un CLI first-class que usa esa API, pensado también para agentes.
    3. D3: cuentas y gestión muy abiertas y opcionales, passwordless por defecto. La doc las especifica; el diseño lo cierra un ADR propio (§6.3).
    4. D4: metadatos incrustados en el propio fichero por un plugin first-class. La doc los especifica; el diseño lo cierra un ADR propio (§6.4).
    5. D5: runtime de despliegue agnóstico y versionado global + por servicio (dec-0123, PROPOSED en w7/release-eng). La doc lo refleja y no lo redecide.
  • Cita:
    • r16: frontier salvo blocker concreto (Fumadocs sobre TanStack Start, generadores propios donde no hay herramienta).
    • r17: un contenedor por authority (una referencia HTTP por servicio).
    • r18 / dec-0116: contratos canónicos TypeBox 1.x / JSON Schema en @styx/api-contracts, de los que sale el OpenAPI.
    • r20 §2.3: contract-first; la doc es otra proyección del contrato, nunca su fuente.
    • r28: cada capa termina en un consumidor real (el guard y el lector son los consumidores del árbol de contenido).
    • r31 D1: verdicts por item; el contador de tests no es evidencia.
    • dec-0112 §1: ningún claim comparativo contra Jellyfin sin la suite WC-JF12.
    • dec-0117, dec-0118, dec-0119, dec-0120, dec-0121: lo que la referencia de seguridad, bus, IPC e ingesta tiene que reflejar.
    • dec-0123 (PROPOSED): versión del tren para el campo since, CLI styx compartido y docs de instalación/operación (RE9).

1. Contexto

Hoy la documentación de Styx es de gobernanza interna (docs/: ADRs, tickets, planes, evidencia, handoffs) y su lector es el equipo y los agentes que lo construyen. No hay docs de producto. La referencia de interfaces existe repartida y sin proyección publicable:

  • GET /openapi/json sólo en catalog-svc (apps/catalog-svc/src/transport/http/app.ts, con toOpenAPISchema de @elysia/openapi). Los otros cinco servicios no lo exponen.
  • BUS_ROUTES en packages/api-contracts/src/bus/routes.ts ya es la tabla única de la que salen policies, ACL de NATS y el espejo Zig (bun run gen:bus, check:bus). No sale documentación.
  • Las especificaciones humanas de protocols/* son canónicas y no tienen sitio publicable.
  • El TSDoc existe con desigual cobertura y nada lo valida.
  • Los módulos Zig públicos (media-core, media-daemon, transmux, libav-bridge) y los SDK consumidos (zkit, spire, conduit) tienen /// sin extractor.
  • apps/cli es un README sin código, y el CLI que existe (styx-upload) es de ingesta.
  • La configuración por variables de entorno no tiene un esquema único.

Si la doc de producto se escribe al final, describe lo que salió y no lo que se prometió, y la referencia escrita a mano se desvía del código al primer cambio. Las dos cosas son el "verde falso" de r31 aplicado a la documentación. waxin pide lo contrario: que la doc sea el contrato y que un gate no pueda cerrar sin ella.

2. Decisión (resumen)

  1. El contenido de producto vive en apps/docs/content/ con las convenciones de Fumadocs (MDX + frontmatter + meta.json). La app Fumadocs llegará después en apps/docs/ (§3). docs/ sigue siendo gobernanza interna y check:docs-layout no cambia.
  2. Arquitectura de información Diátaxis (tutoriales, guías, referencia, explicación) más cuatro secciones: producto, operación, contribución y roadmap/decisiones (§4).
  3. Cada página lleva un frontmatter de contrato con nodos, gates y ADRs a los que está atada y su estado: especificado | implementado | verificado (§5). La doc describe el sistema objetivo, y el estado dice cuánto de eso es ya verdad.
  4. Todo lo generable se genera, con un generador por fuente y modo --check de drift (§7).
  5. Dos guards nuevos: check:docs-contract (cobertura, coherencia de estado con el model y el manifest, drift) y check:headless-parity (UI ↔ contrato ↔ SDK ↔ CLI) (§8).
  6. El gate.manifest exige docu: un item no pasa de open sin páginas que lo citen en el estado correspondiente, y una página no es verificado sin el item en pass (§9).

3. Ubicación y stack

3.1 Dónde vive cada cosa

QuéDóndePor qué
Contenido de producto (MD/MDX + meta.json)apps/docs/content/docs/**Es la carpeta de contenido por defecto de Fumadocs MDX, y apps/ es la casa de las superficies de cliente.
Referencia generadaapps/docs/content/docs/referencia/** con generated: en el frontmatterSe commitea (se lee en GitHub y en el editor sin build) y el guard detecta el drift.
Artefactos intermedios (OpenAPI JSON, JSON de símbolos TS/Zig)apps/docs/generated/**Entrada de fumadocs-openapi y de las tablas; también se commitean y se comprueban.
Generadoresscripts/docs/*.ts, más el extractor Zig en native/zig/tools/Siguen el patrón de scripts/gen-bus.ts.
App Fumadocs (layout, MDX components, búsqueda)apps/docs/ (src/, source.config.ts)Después del lock y de elegir plantilla (P1). Hasta entonces no hay código de app (r28).
Gobernanza interna (ADRs, tickets, evidencia)docs/ sin cambiosLa sección "Decisiones" y "Roadmap" del sitio se generan desde aquí. No se duplican a mano.

apps/docs no es un servicio Elysia. Mientras no tenga app, es un paquete de contenido con su package.json (sólo los scripts de generación y de check) y su CLAUDE.md anidado, y entra en el mapa de apps/CLAUDE.md en el mismo cambio que lo crea (ticket 01). Los docs de instalación y operación que escribe la lane D de w7/release-eng (RE9) se absorben en operacion/ al integrar: este ADR fija dónde viven, no su contenido.

3.2 Stack (versiones a la fecha; se fijan en el lockfile al crear la app)

  • Fumadocs 16.x (fumadocs-core, fumadocs-ui), fumadocs-mdx 15.x. Licencia MIT. Soporta Next.js, TanStack Start, React Router, Waku y Astro. Decidido (waxin, 2026-10-02, P1): TanStack Start, el mismo stack de apps/web (React 19 + TanStack Start + Vite + Tailwind): un solo modelo mental, y la frontera de apps/web/CLAUDE.md se reutiliza.
  • fumadocs-openapi 12.x. generateFiles({ per: 'operation', groupBy: 'tag' }) genera una página MDX por operación, con playground y ejemplos, más meta.json e índices. Documenta la integración con TanStack Start. Su peer @scalar/api-client-react sólo entra en la app.
  • fumadocs-typescript 5.x (AutoTypeTable, remarkAutoTypeTable) para tablas de tipos dentro de páginas escritas a mano. Lee TSDoc (@internal, @remarks).
  • TypeDoc 0.28 + typedoc-plugin-markdown 4.x para la referencia completa de cada paquete público. TypeDoc 0.28 admite TypeScript hasta 6.0. El repo tiene typescript 5.x junto a tsgo, que no tiene API JS: TypeDoc usa el typescript de JS. Licencias Apache-2.0 y MIT. Sin GPL.
  • Zig: -femit-docs produce un autodoc HTML/WASM, no Markdown, y no valida nada. Por eso hay un extractor propio sobre std.zig.Ast (§7.4).
  • i18n: Fumadocs admite pagina.en.mdx (notación de punto). Idioma canónico español (el del repo), con en como traducción posterior. Lo decide waxin (P4).

4. Arquitectura de información

Diátaxis separa cuatro necesidades del lector: aprender (tutorial), resolver (guía), consultar (referencia) y entender (explicación). Se añaden cuatro secciones que Diátaxis no cubre y que este proyecto necesita.

apps/docs/content/docs/
├── index.mdx                       qué es Styx, a quién sirve, por dónde empezar
├── meta.json                       orden de secciones (root folders de Fumadocs)
├── producto/                       visión, experiencia, comparativa (sin claims sin WC-JF12, dec-0112)
├── tutoriales/                     primer arranque · primera biblioteca · primera película en Chrome
│                                   · invitar a la familia · TV por quick connect · primer script con API key
├── guias/
│   ├── cuentas/                    hogares · perfiles · invitaciones · passkeys/OIDC · login con password
│   │                               (opt-in) · API keys · quick connect · sesiones y revocación
│   ├── biblioteca/                 raíces · escaneo · metadatos incrustados · importar NFO de Jellyfin
│   │                               · subir ficheros (ingesta conduit) · normalización de formatos
│   ├── reproduccion/               navegadores (Chrome/Canary primero) · pistas · subtítulos · seek
│   ├── cli/                        login · salida --json · exit codes · recetas
│   └── agentes/                    API para agentes · MCP (si se adopta, §6.2)
├── referencia/                     GENERADA salvo índices
│   ├── http/<servicio>/            OpenAPI por servicio (fumadocs-openapi)
│   ├── sdk/<paquete>/              TypeDoc markdown de los paquetes públicos
│   ├── bus/                        un subject por página desde BUS_ROUTES + JSON Schema
│   ├── protocolos/                 session-ipc, capability-token, media-object, schema-versioning
│   ├── zig/<modulo>/               pub decls con /// de los módulos públicos y SDKs consumidos
│   ├── cli/                        comandos, flags, salida, exit codes
│   ├── configuracion/              variables de entorno y config por servicio y daemon
│   ├── errores/                    tipos problem+json (RFC 9457) y códigos de error
│   └── permisos/                   roles × permisos desde @styx/authz
├── explicacion/                    arquitectura (5 planos, Bun decide/Zig ejecuta) · byte runtime ·
│                                   plan de reproducción (r05) · catálogo federado (r08) · seguridad
│                                   (dec-0117/0118/0119) · identidad y cuentas · metadatos en el fichero
│                                   · codec-agnostic (r23)
├── operacion/                      instalar · actualizar · canales · backup · observabilidad ·
│                                   runtimes de despliegue (dec-0123; absorbe RE9 de w7)
├── contribuir/                     cómo escribir una página contrato · guards · generadores
├── roadmap/                        GENERADA desde styx.model.yml: una página por nodo con gates,
│                                   estado y las páginas de doc que lo cubren
└── decisiones/                     GENERADA desde docs/decisions/: una página por ADR, con su
                                    estado LOCKED/PROPOSED/SUPERSEDED y las páginas que lo citan

Reglas de la IA:

  • Una página tiene un tipo Diátaxis (kind en el frontmatter). Si mezcla tutorial y referencia, se parte.
  • roadmap/ y decisiones/ son vistas: el model y docs/decisions/ mandan. Una página escrita a mano no repite el estado de un gate; lo enlaza.
  • La sección producto/ no publica claims comparativos sin la campaña WC-JF12 (dec-0112 §1).

5. Frontmatter de contrato

Campos de Fumadocs (title, description, icon) más un bloque contract. El esquema es TypeBox en apps/docs/content.schema.ts, y source.config.ts lo usará como esquema de la colección cuando exista la app.

---
title: Invitaciones
description: Invitar a alguien a tu servidor o a tu hogar con un enlace o un código.
kind: guia # tutorial | guia | referencia | explicacion | producto | operacion | contribuir
contract:
  nodes: [track/identity] # ≥1 nodo de styx.model.yml (o experiments[])
  gates: ['track/identity:I3'] # <nodeId>:<itemId>, opcional para páginas de explicación/producto
  adrs: [dec-0118] # ≥0 ADRs de docs/decisions/; un PROPOSED se cita con su id igual
  status: especificado # especificado | implementado | verificado
  since: null # versión del tren (dec-0123) en la que pasó a implementado; null antes
  audience: [usuario, operador] # usuario | operador | desarrollador | agente
  generated: null # o { source, generator, inputHash } en páginas generadas
---

5.1 Semántica del estado

EstadoSignificaCondición mecánica (la comprueba check:docs-contract)
especificadoDescribe el comportamiento objetivo. Lo que dice obliga, aunque no exista aún.Los nodos, gates y ADRs citados existen. La página lleva el aviso "especificado" que el sitio pinta como badge.
implementadoEl comportamiento existe en un production path.Algún nodo citado está in_progress o done. Si la página documenta una operación, ruta, subject, comando o variable, su referencia generada existe (la operación está en el OpenAPI, el comando en el CLI, etc.). since no es null cuando el tren existe.
verificadoUn verificador ≠ autor lo comprobó contra el production path.Todos los gate items citados están pass en el model, proyectado del manifest. Y al revés: si todos los items citados están pass, la página tiene que ser verificado. Que la doc se quede atrás también es rojo.

Una página no baja de estado sin que baje lo que cita (por ejemplo, un gate reabierto). Una regresión de estado sin causa en el model es rojo.

5.2 Páginas generadas

Llevan generated: { source, generator, inputHash }, donde inputHash es el sha256 de la entrada (el OpenAPI JSON, el fichero de contrato, el JSON del extractor). No se editan a mano: el guard regenera y compara byte a byte. Su status se hereda del nodo dueño de la fuente con la misma tabla, y nunca pasa de implementado por sí mismo: la referencia de una interfaz dice qué hay, no que esté verificado. Pasa a verificado sólo si el gate item que la cita en el manifest (§9) lo está.

6. El sistema objetivo que la doc especifica

Este ADR no decide el diseño de producto. Fija que estas áreas tienen páginas especificado desde el primer día, atadas a su nodo, y nombra el ADR que cerrará cada diseño. Mientras ese ADR no exista, la página dice "especificado — diseño pendiente de ADR" y enlaza la pregunta. Una página especificada sin ADR detrás no se puede promover a implementado.

6.1 Headless first-class (D2)

  • Registro de operaciones en @styx/api-contracts (src/operations/): cada capacidad de producto es una operación con id estable (library.scan, account.invite.create…), su ruta HTTP (o null si es sólo bus), su permiso de @styx/authz, su método de SDK, su comando de CLI y una marca agentSafe (sin efectos destructivos sin confirmación). Es la misma idea que BUS_ROUTES: una tabla única de la que salen OpenAPI, SDK, CLI, la página de referencia y el guard de paridad.
  • SDK: @styx/clients crece de Eden (r52) a un SDK público tipado por operación. La web lo consume desde sus server functions (BFF, dec-0118: el navegador no tiene tokens).
  • CLI styx: el mismo binario que dec-0123 define para instalar y actualizar. Este ADR añade los grupos de API (styx library …, styx account …, styx invite …, styx key …, styx session …, styx server …). Contrato para agentes: --json en todos los comandos con un esquema por comando, exit codes estables y documentados (0 ok, 2 uso, 3 auth, 4 no encontrado, 5 conflicto, 6 no disponible, 7 policy denegada; la tabla final la fija el ADR del CLI), --yes y --dry-run en lo destructivo y sin prompts cuando no hay TTY.
  • MCP, como propuesta: un servidor MCP generado del registro de operaciones (herramientas = operaciones agentSafe), autenticado con API key de scope reducido. No se lockea aquí (P5).
  • ADR que lo cierra: headless, SDK y CLI (siguiente número libre al redactarlo).

6.2 Cuentas, hogares, perfiles y acceso (D3)

Muy abierto y opcional: cada despliegue elige su forma, y ninguna se impone.

  • Cuenta = identidad que inicia sesión (sujeto OIDC). Puede vivir sola o dentro de un hogar (grupo opcional con propietario y administradores).
  • Perfiles opcionales por cuenta, al estilo Netflix: una cuenta familiar con un perfil por persona (estado de visionado, idioma y clasificación por perfil, PIN opcional), o una cuenta por persona. Las dos formas pueden convivir en el mismo servidor.
  • Invitaciones al estilo Wizarr para Jellyfin: enlace o código con caducidad, número de usos, bibliotecas, rol, hogar de destino, y si crea cuenta nueva o perfil. Al aceptarla se registra la cuenta en el IdP.
  • Passwordless por defecto: OIDC contra Pocket ID u otro IdP con passkeys (lo que track/identity ya implementa: OIDC real contra PocketID, dec-0113/dec-0118). El login con password sólo existe si se habilita por env/config, y viene apagado por defecto.
  • API keys por cuenta, con scopes, caducidad, último uso y revocación, para scriptear la API. No se aceptan desde contexto de navegador (dec-0118 §2.1: el navegador no tiene tokens portadores).
  • Login del CLI y quick connect para TV y dispositivos: flujo de autorización de dispositivo (RFC 8628) servido por identity-svc. El dispositivo muestra un código numérico y un QR. El usuario lo aprueba desde una sesión ya iniciada y elige el perfil. Es independiente de que el IdP soporte el device grant.
  • ADR que lo cierra: cuentas, hogares, perfiles, invitaciones y acceso de dispositivos. Tiene que respetar dec-0118 y los gaps abiertos de track/identity/sec (SW20: proxy /auth, e2e con PocketID).

6.3 Metadatos en el propio fichero (D4)

  • Plugin first-class metadata-embed: al añadir algo a la biblioteca busca metadatos y portada y los incrusta en el fichero (MP4/ISOBMFF moov/udta/meta/ilst con covr; Matroska Tags + Attachments). Así el catálogo es semi-stateless respecto al fichero. La sincronización fichero ↔ catálogo se decide después.
  • El NFO de Jellyfin es una vía de importación (metadata-local-nfo), no el almacén.
  • Restricciones que la página explicacion/metadatos-en-el-fichero hace explícitas y que el ADR tiene que resolver:
    • escribir el fichero es una escritura del data plane: la hace el daemon con raíz fd-relativa y scope de escritura (dec-0117 I3/I7), y los bytes no pasan por JS (r01);
    • la huella de contenido (SC1 de dec-0114) tiene que excluir las cajas de metadatos, o cada incrustación cambia la identidad del asset;
    • reescribir moov sin mover mdat exige padding (free).
  • Biblioteca de referencia de waxin: x264/x265, 4K y 1080p, normalizada (normalmente no MKV), reproducida en Chrome y Chrome Canary. Las guías de reproducción empiezan por Chrome.
  • ADR que lo cierra: metadatos incrustados en el fichero.

6.4 Despliegue y versiones (D5)

operacion/ y el campo since reflejan dec-0123 (tren global + SemVer por componente, manifiesto firmado, canales nightly/stable, CLI styx). Mientras dec-0123 esté PROPOSED, sus páginas son especificado y since es null.

7. Generadores (uno por fuente)

Todos siguen el patrón de gen:bus: bun run docs:gen escribe y bun run docs:gen --check regenera en memoria y falla si difiere. El orden es determinista: claves ordenadas y sin timestamps.

#Fuente canónicaGeneradorSalidaValidación que hace fallar
7.1Rutas Elysia 2 de cada servicio con esquemas de @styx/api-contractsscripts/docs/gen-openapi.ts: construye cada app con la app de captura (app.aot.ts, la misma createHttpApp sin infra) y llama a toOpenAPISchema de @elysia/openapi. Después corre fumadocs-openapi generateFiles({ per: 'operation', groupBy: 'tag' }).apps/docs/generated/openapi/<svc>.json + referencia/http/<svc>/**Operación sin summary/description, sin operationId igual al id del registro (§6.1), sin respuestas de error problem+json, o con un esquema anónimo. Ruta sin entrada en el registro de operaciones.
7.2TSDoc de los paquetes públicos (@styx/api-contracts, @styx/clients/SDK, @styx/authz, @styx/plugin-sdk, @styx/source-sdk, @styx/domain, @styx/bus)TypeDoc + typedoc-plugin-markdown por entrypoint de exportsreferencia/sdk/<pkg>/**validation.notDocumented: true, invalidLink: true y treatWarningsAsErrors: true. Una exportación pública sin TSDoc es error. La adopción se hace con un trinquete por paquete (baseline que sólo baja), como audit:safety.
7.3BUS_ROUTES + contratos IBusContract (subject, versión, tope, JSON Schema, allowlist, transporte)scripts/docs/gen-bus-docs.ts, que reutiliza el cargador de scripts/gen-bus.tsreferencia/bus/**Subject sin descripción, o contrato sin JSON Schema de request/reply.
7.4/// y //! de los módulos Zig públicos (media-core, media-daemon en su superficie IPC/config, transmux, libav-bridge) y de los SDK consumidos (zkit, spire, conduit) en el hash fijado en build.zig.zonnative/zig/tools/docs-extract.zig sobre std.zig.Ast: recorre los pub decls alcanzables desde el root de cada módulo y emite JSON (nombre, firma, doc, error set). scripts/docs/gen-zig-docs.ts lo convierte en MDX.apps/docs/generated/zig/*.json + referencia/zig/**Un pub decl sin /// en un módulo público es error, con el mismo trinquete que 7.2. Los SDK externos se documentan, pero su cobertura no bloquea aquí: bloquea en su repo.
7.5protocols/* (spec humana canónica + vectors.json)scripts/docs/sync-protocols.ts: copia con frontmatter y enlaza los vectores. La spec no se reescribe.referencia/protocolos/**Protocolo sin schemaVersion, sin política de compatibilidad o sin fixtures (la regla de protocols/CLAUDE.md hecha mecánica).
7.6Registro de comandos del CLI (derivado del registro de operaciones + comandos de instalación de dec-0123)styx docs --format mdx, emitido por el propio CLIreferencia/cli/**Comando sin descripción, sin esquema de --json o sin exit codes declarados.
7.7Esquema único de configuración por servicio (src/config.schema.ts, TypeBox) y del daemon (tabla Zig config_schema.zig)scripts/docs/gen-config.tsreferencia/configuracion/**Variable sin descripción, default o marca de secreto. Y un guard aparte (check:env-schema): una lectura de process.env/Bun.env/std.process.getEnvVarOwned fuera del módulo de config es error.
7.8Problem types (problem.ts) y códigos de error de los contratosscripts/docs/gen-errors.tsreferencia/errores/**Problem type sin título ni documentación.
7.9ROLES, permisos y policies de @styx/authzscripts/docs/gen-permissions.tsreferencia/permisos/**—
7.10styx.model.yml y docs/decisions/*.mdscripts/docs/gen-roadmap-adrs.tsroadmap/**, decisiones/**Nodo o ADR LOCKED sin ninguna página que lo cite (es la cobertura de §8.1).

7.11 Spec de cobertura congelada de los escáneres estáticos (nota del track, 2026-10-02)

Nota del track track/docs (rama w9/docs-gen). No cambia el estado de este ADR: sigue PROPOSED.

Dos generadores leen código con reconocimiento de formas y no con el compilador: scripts/docs/env-scan.ts (lecturas del entorno en TS y en Zig, §7.7) y native/zig/tools/docs_extract.zig (superficie pub de Zig, §7.4). En siete rondas de revisión adversarial cada ronda encontró una forma nueva de escribir lo mismo que el escáner no veía. Eso no converge, así que la cobertura se congela en una lista cerrada con una regla para lo que queda fuera:

Forma no reconocida pero detectable = opaca/roja; formas indetectables por análisis estático = fuera de alcance, cubiertas por revisión y por el guard de cobertura del registro de operaciones.

  • Reconocida (lectura / recorrida): el escáner la convierte en nombres o en declaraciones.
  • Detectable y no reconocida (opaca / no-soportada): el escáner la ve y no la puede resolver. Es una violación (rojo) en docs:check, nunca un hueco silencioso.
  • Fuera de alcance: el análisis estático no la puede ver (flujo de datos, código construido en tiempo de ejecución, reflexión, otra vía de lectura). Las cubre la revisión. El guard del registro de operaciones (check:headless-parity, ticket 08) y check:env-schema (ticket 06, el entorno sólo se lee en el módulo de config) están pendientes. Mientras tanto la cobertura es sólo la revisión.

La lista es ejecutable: scripts/docs/coverage-spec.ts lleva un ejemplo de cada forma y test/docs-coverage-spec.test.ts lo corre contra el escáner real, con el veredicto de la tabla. También comprueba que la lista de ids es la congelada y que este apartado y los tickets 04 y 06 recogen cada id y la regla. Las formas fuera de alcance también se ejecutan: el test exige que el escáner no las vea, para que el borde no se mueva sin que nadie lo note. Una forma nueva entra cambiando a la vez la spec, el test, el ticket y este apartado. Si un revisor encuentra otra forma indetectable, se añade aquí como fuera de alcance; no hace falta otra ronda de escáner.

env-scan, TS

  • ENV-TS-L1 (lectura): process.env.X, Bun.env.X, import.meta.env.X, también con ?. y con espacios o saltos alrededor del ..
  • ENV-TS-L2 (lectura): process.env['X'] / process.env[`X`] (literal con un nombre válido).
  • ENV-TS-L3 (lectura): <algo>env.X, e.X y <algo>env['X'] (el objeto de entorno que el bootstrap pasa hacia abajo).
  • ENV-TS-L4 (lectura): envVar('X') (OTel).
  • ENV-TS-L5 (lectura): const { X, Y: y, Z = 'd' } = process.env (también Bun.env, import.meta.env).
  • ENV-TS-L6 (lectura): alias del objeto de entorno: process['env'], process?.env, import { env as E } from 'node:process' | 'process' | 'bun', import * as p from 'node:process' y const { env: E } = process | Bun.
  • ENV-TS-L7 (lectura): helper con el nombre como parámetro (function f(name) { process.env[name] }, env[name]), también exportado y llamado desde otro fichero (import { f as g }); la llamada con un literal es la lectura.
  • ENV-TS-L8 (lectura): consumidores del entorno entero declarados en ENV_CONSUMERS (resolveSecretEnv, defaultValidationDetail) y lectores dinámicos declarados en ENV_DYNAMIC_READERS (loadClientAddressPolicy(env, "P") → P_TRUSTED_PROXIES, P_PROXY_PROOF, P_DIRECT_CLIENTS).
  • ENV-TS-O1 (opaca): process.env[k] con k que no es un literal ni un parámetro de la función.
  • ENV-TS-O2 (opaca): process.env / Bun.env entero pasado o guardado fuera de ENV_CONSUMERS.
  • ENV-TS-O3 (opaca): desestructuración con ...resto (de process.env o del objeto process / Bun).
  • ENV-TS-O4 (opaca): el objeto process / Bun entero pasado o guardado, o indexado con una clave que no es un literal.
  • ENV-TS-O5 (opaca): <algo>env[k] con una clave que no es un literal con un nombre válido ni un parámetro (fuera de ENV_DYNAMIC_READERS); escribir env[k] = v no cuenta.
  • ENV-TS-O6 (opaca): carga dinámica del módulo de entorno: require('node:process' | 'process' | 'bun'), import('…').
  • ENV-TS-O7 (opaca): globalThis[…] (cualquier clave).
  • ENV-TS-O8 (opaca): helper de entorno de otro fichero usado como valor (usar(helper), export const f = helper, export default helper).
  • ENV-TS-O9 (opaca): helper de entorno llamado con un nombre que no es un literal ni un parámetro.
  • ENV-TS-O10 (opaca): process.env.<nombre> con un nombre que no es [A-Z][A-Z0-9_]*.
  • ENV-TS-F1 (fuera-de-alcance): el objeto de entorno ya recibido bajo un nombre que no acaba en env/Env ni es e (flujo de datos: el paso de process.env en el sitio de llamada sí es rojo).
  • ENV-TS-F2 (fuera-de-alcance): leer el entorno por otra vía que no es el objeto de entorno: /proc/self/environ, un proceso hijo (printenv).
  • ENV-TS-F3 (fuera-de-alcance): código construido en tiempo de ejecución (eval, new Function con un texto calculado).

env-scan, Zig

  • ENV-ZIG-L1 (lectura): familia getenv* / envOr* / envInt* / envBool* / parseEnv* con un literal (getenvInt(u64, "X"): tras un tipo, el segundo).
  • ENV-ZIG-L2 (lectura): getEnvVarOwned(a, "X"), hasEnvVar(a, "X"), hasEnvVarConstant("X").
  • ENV-ZIG-L3 (lectura): secret_file.load*(a, "X") (lee X_FILE).
  • ENV-ZIG-L4 (lectura): m.get("X") sobre un mapa de entorno: parámetro tipado Environ.Map, getEnvMap(…) o init.environ_map.
  • ENV-ZIG-L5 (lectura): helper con el nombre como parámetro (fn f(comptime name: …) que llama a una lectura con name), también pub fn llamado desde otro fichero.
  • ENV-ZIG-L6 (lectura): consumidor del entorno entero declarado en ENV_CONSUMERS (ResumeState.init(…, environ) → XDG_STATE_HOME, HOME).
  • ENV-ZIG-O1 (opaca): lectura con un nombre que no es un literal ni un parámetro (campo, índice, llamada, concatenación, identificador).
  • ENV-ZIG-O2 (opaca): el entorno entero: std.os.environ, std.c.environ, std.posix.environ, extern var environ.
  • ENV-ZIG-O3 (opaca): una función de lectura usada como valor, con cualquier calificador (const g = std.process.getEnvVarOwned;).
  • ENV-ZIG-O4 (opaca): un mapa de entorno pasado a algo que no es una función del mismo fichero ni un consumidor declarado, o .get(…) con un nombre que no es un literal.
  • ENV-ZIG-F1 (fuera-de-alcance): leer /proc/self/environ como fichero.
  • ENV-ZIG-F2 (fuera-de-alcance): obtener la función de lectura por un nombre en un texto (@extern(…, .{ .name = "getenv" }), dlsym).
  • ENV-ZIG-F3 (fuera-de-alcance): un mapa de entorno que llega sin tipo Environ.Map explícito ni de getEnvMap / environ_map (inferido de otra llamada).

docs_extract (Zig)

  • ZIG-DOC-R1 (recorrida): pub fn, pub const, pub var con su ///.
  • ZIG-DOC-R2 (recorrida): pub const X = struct/enum/union/opaque {…}: sus miembros pub salen como X.y.
  • ZIG-DOC-R3 (recorrida): pub const x = @import("x.zig"): se recorre x.zig con el prefijo x..
  • ZIG-DOC-R4 (recorrida): pub const X = @import("x.zig").Y: se documenta Y como X (con el /// del alias si Y no tiene).
  • ZIG-DOC-R5 (recorrida): genérico pub fn X(…) type con un return de primer nivel de su cuerpo que devuelve un contenedor: sus pub salen como X.y.
  • ZIG-DOC-R6 (recorrida): if / switch (anidados y entre paréntesis) con contenedores en las ramas, en pub const X = … y en el return del genérico.
  • ZIG-DOC-R7 (recorrida): alias local: pub const X = Local (un const del mismo fichero, hasta 8 saltos; un ciclo no se recorre dos veces) y pub const X = m.Y con const m = @import("m.zig").
  • ZIG-DOC-N1 (no-soportada): contenedor con miembros pub en cualquier otra posición del valor de un pub const / pub var (bloque etiquetado, @as, comptime, orelse, argumento de una llamada…).
  • ZIG-DOC-N2 (no-soportada): genérico con un return que no es el reconocido (anidado en un if / bloque, o tras el primero) y devuelve un contenedor con pub.
  • ZIG-DOC-F1 (fuera-de-alcance): tipos construidos por reflexión (@Type(…)): no hay pub en el texto.
  • ZIG-DOC-F2 (fuera-de-alcance): instancia de un genérico de otro módulo (pub const Q = std.ArrayList(u8)): sus miembros se documentan donde vive el genérico.
  • ZIG-DOC-F3 (fuera-de-alcance): @import de un módulo del build (@import("zkit")): queda como namespace; los SDK se documentan como módulo propio en gen-zig-docs.ts.
  • ZIG-DOC-F4 (fuera-de-alcance): alias de un tipo que no es local ni de un @import relativo (pub const Al = std.mem.Allocator).

8. Guards

8.1 check:docs-contract

Se añade a bun run check:roadmap (scripts/run-all-checks.ts) y a guard.yml tras el lock. Antes corre en modo informe (exit 0 con la lista de violaciones). Comprueba:

  1. Esquema: toda página de apps/docs/content/docs/** tiene frontmatter válido contra content.schema.ts, y todo directorio tiene meta.json.
  2. Referencias vivas: cada nodes[] existe en el model (nodos o experiments), cada gates[] existe como item, y cada adrs[] existe en docs/decisions/.
  3. Cobertura:
    • todo nodo queued | in_progress | done (no vision-locked, no experiments) tiene al menos una página escrita a mano que lo cita;
    • todo gate item del model tiene al menos una página que lo cita;
    • todo ADR LOCKED tiene al menos una página no generada que lo cita, o una entrada en apps/docs/adr-exemptions.yml con motivo (ADRs de gobernanza puramente internos, como dec-0099..dec-0102).
  4. Coherencia de estado: la tabla de §5.1, en las dos direcciones.
  5. Drift: docs:gen --check de los generadores de §7.
  6. Higiene: sin contadores agregados de gates ("6/8", r31 D1) y sin estado de gate copiado a mano (el estado vive en roadmap/, generado).

Mutantes obligatorios en su ticket: una página verificado con su item open → rojo; un item pass con su página especificado → rojo; un nodo nuevo sin página → rojo; un OpenAPI con una ruta nueva sin regenerar → rojo.

8.2 check:headless-parity

  1. Cada server function de apps/web/src/server/** llama sólo a operaciones del SDK. Una llamada fetch/Eden directa a un servicio fuera del SDK es error.
  2. Cada operación del registro tiene: ruta HTTP en el OpenAPI de su servicio (o busOnly con motivo), método de SDK y comando de CLI. Si no tiene CLI, lleva cliExempt con motivo, revisable.
  3. Cada operación usada por la UI está en el registro. Lo que la UI ofrece existe headless.
  4. Cada operación agentSafe: false exige confirmación en el CLI (--yes), y el test del CLI lo comprueba.

Mutantes: una server function con fetch directo → rojo; una operación sin comando CLI y sin cliExempt → rojo.

8.3 Relación con los guards existentes

check:bus sigue mandando sobre el espejo Zig. check:docs-contract sólo consume su salida. check:docs-layout no cambia, porque apps/docs no está bajo docs/. check:context añade apps/docs/CLAUDE.md a sus superficies cuando exista.

9. Cómo un gate exige docu

  1. Cada item de gate.manifest.yml gana el campo docs: [<rutas de página>].
  2. GUARD 6 (scripts/check-gates.ts) se extiende: un item no-open exige docs no vacío, cada ruta existe y cita ese item en contract.gates, y el estado de las páginas cumple la tabla de §5.1 (pass ⇒ verificado; partial/provisional ⇒ al menos implementado).
  3. El verificador del item (≠ autor, como hoy) firma también la doc: lee las páginas contra el production path. Si la doc promete algo que el código no hace, el item no pasa. Así la doc es el contrato que el código cumple, y no al revés.
  4. Trinquete de adopción: los items que ya están en pass cuando se lockea este ADR entran en scripts/docs-contract.baseline.json sin docs. La lista sólo puede bajar (un item sale cuando gana su doc), y un item nuevo no puede entrar. Es el mismo mecanismo que el baseline de clases r31.
  5. Nodos nuevos: un nodo que entra en el model en estado queued necesita al menos una página especificado en el mismo cambio (§8.1.3). La docu nace con el nodo y no al final.

10. Preguntas para waxin (bloquean el lock)

  • P1 — Framework del sitio: TanStack Start (recomendado: mismo stack que apps/web) o Next.js. Y la plantilla de Fumadocs (Docs layout por defecto, Notebook, o una propia). Resuelta (waxin, 2026-10-02): TanStack Start. La plantilla (Docs layout por defecto, Notebook o una propia) se elige con /prototype cuando exista la app, no aquí.
  • P2 — Ubicación: apps/docs/content (recomendado) o un paquete packages/docs-content separado de la app.
  • P3 — Severidad de TSDoc/Zig: notDocumented como error con trinquete por paquete (recomendado), o como error desde el día uno en todos los paquetes públicos.
  • P4 — Idioma: español canónico + inglés después (recomendado), o inglés canónico.
  • P5 — MCP: adoptar ya el servidor MCP generado del registro de operaciones, o dejarlo como propuesta hasta que el CLI exista.
  • P6 — Bloqueo: que check:docs-contract bloquee CI desde el lock, con trinquete (recomendado), o un periodo en modo informe.

11. Consecuencias

  • Cada lane que abre o cierra un gate item escribe o actualiza su página en el mismo cambio. El coste se paga de forma continua y no al final.
  • La referencia no se escribe a mano. El esfuerzo va al TSDoc, a los /// y a las descripciones de los contratos, que además mejoran el código.
  • El registro de operaciones (§6.1) es trabajo nuevo en @styx/api-contracts. Sin él no hay paridad mecánica entre UI, SDK y CLI.
  • Riesgo: que los guards se vuelvan ceremonia. Mitigación: cada regla tiene mutante y el verificador lee la página contra el código (r31). El guard es una precondición, no una prueba de calidad.
  • Lo que no decide: el diseño de cuentas, del CLI y de los metadatos incrustados (§6, cada uno con su ADR); la plantilla visual; el contenido de dec-0123.