Vista generada de dec-0124: Documentación como contrato: Fumadocs, Diátaxis, frontmatter de contrato, referencia generada y guards que impiden posponerla
docs/decisions/dec-0124-docs-como-contrato.mdVista generada desde
docs/decisions/dec-0124-docs-como-contrato.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.
| Campo | Valor |
|---|---|
| Estado | PROPOSED |
| Fecha | 2026-10-01 |
| Fichero | docs/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)
Leído de docs/decisions/dec-0124-docs-como-contrato.md, el fichero canónico.
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.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.w9/docs). Nodo track/docs, alta propuesta en styx.model.yml.
Plan en docs/track/docs/plans/00-plan.md.dec-0123,
PROPOSED en w7/release-eng). La doc lo refleja y no lo redecide.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).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.protocols/* son canónicas y no tienen sitio publicable.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.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.
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.especificado | implementado | verificado (§5). La doc describe el
sistema objetivo, y el estado dice cuánto de eso es ya verdad.--check de drift (§7).check:docs-contract (cobertura, coherencia de estado con el model y
el manifest, drift) y check:headless-parity (UI ↔ contrato ↔ SDK ↔ CLI) (§8).open sin páginas que lo citen en el
estado correspondiente, y una página no es verificado sin el item en pass (§9).| Qué | Dónde | Por 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 generada | apps/docs/content/docs/referencia/** con generated: en el frontmatter | Se 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. |
| Generadores | scripts/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 cambios | La 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.
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-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.-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).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).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 citanReglas de la IA:
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.producto/ no publica claims comparativos sin la campaña WC-JF12 (dec-0112 §1).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
---| Estado | Significa | Condición mecánica (la comprueba check:docs-contract) |
|---|---|---|
especificado | Describe 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. |
implementado | El 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. |
verificado | Un 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.
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á.
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.
@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.@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).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.agentSafe), autenticado con API key de scope reducido. No se lockea aquí (P5).Muy abierto y opcional: cada despliegue elige su forma, y ninguna se impone.
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.dec-0118 §2.1: el navegador no tiene tokens
portadores).dec-0118 y los gaps abiertos de track/identity/sec (SW20: proxy /auth,
e2e con PocketID).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.metadata-local-nfo), no el almacén.explicacion/metadatos-en-el-fichero hace explícitas y que el ADR
tiene que resolver:
dec-0117 I3/I7), y los bytes no pasan por JS (r01);dec-0114) tiene que excluir las cajas de metadatos, o
cada incrustación cambia la identidad del asset;moov sin mover mdat exige padding (free).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.
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ónica | Generador | Salida | Validación que hace fallar |
|---|---|---|---|---|
| 7.1 | Rutas Elysia 2 de cada servicio con esquemas de @styx/api-contracts | scripts/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.2 | TSDoc 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 exports | referencia/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.3 | BUS_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.ts | referencia/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.zon | native/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.5 | protocols/* (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.6 | Registro de comandos del CLI (derivado del registro de operaciones + comandos de instalación de dec-0123) | styx docs --format mdx, emitido por el propio CLI | referencia/cli/** | Comando sin descripción, sin esquema de --json o sin exit codes declarados. |
| 7.7 | Esquema único de configuración por servicio (src/config.schema.ts, TypeBox) y del daemon (tabla Zig config_schema.zig) | scripts/docs/gen-config.ts | referencia/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.8 | Problem types (problem.ts) y códigos de error de los contratos | scripts/docs/gen-errors.ts | referencia/errores/** | Problem type sin título ni documentación. |
| 7.9 | ROLES, permisos y policies de @styx/authz | scripts/docs/gen-permissions.ts | referencia/permisos/** | — |
| 7.10 | styx.model.yml y docs/decisions/*.md | scripts/docs/gen-roadmap-adrs.ts | roadmap/**, decisiones/** | Nodo o ADR LOCKED sin ninguna página que lo cite (es la cobertura de §8.1). |
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.
lectura / recorrida): el escáner la convierte en nombres o en declaraciones.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.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).check:docs-contractSe 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:
apps/docs/content/docs/** tiene frontmatter válido contra
content.schema.ts, y todo directorio tiene meta.json.nodes[] existe en el model (nodos o experiments), cada
gates[] existe como item, y cada adrs[] existe en docs/decisions/.queued | in_progress | done (no vision-locked, no experiments) tiene al menos
una página escrita a mano que lo cita;apps/docs/adr-exemptions.yml con motivo (ADRs de gobernanza puramente internos, como
dec-0099..dec-0102).docs:gen --check de los generadores de §7.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.
check:headless-parityapps/web/src/server/** llama sólo a operaciones del SDK. Una
llamada fetch/Eden directa a un servicio fuera del SDK es error.busOnly con
motivo), método de SDK y comando de CLI. Si no tiene CLI, lleva cliExempt con motivo,
revisable.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.
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.
gate.manifest.yml gana el campo docs: [<rutas de página>].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).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.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.apps/web) o
Next.js. Y la plantilla de Fumadocs (Docs layout por defecto, Notebook, o una propia)./prototype cuando exista la app, no aquí.apps/docs/content (recomendado) o un paquete packages/docs-content
separado de la app.notDocumented como error con trinquete por paquete
(recomendado), o como error desde el día uno en todos los paquetes públicos.check:docs-contract bloquee CI desde el lock, con trinquete (recomendado),
o un periodo en modo informe./// y a las
descripciones de los contratos, que además mejoran el código.@styx/api-contracts. Sin él no hay
paridad mecánica entre UI, SDK y CLI.dec-0123.dec-0123
Vista generada de dec-0123: Release engineering: tren versionado + componentes SemVer, artefactos únicos, manifiesto firmado, canales nightly/stable, runtime adapters y CLI `styx` de instalación y actualización
dec-0125
Vista generada de dec-0125: Cuentas, hogares, perfiles, invitaciones y acceso de dispositivos