dec-0112

Vista generada de dec-0112: La suite WC-JF12-xx contra Jellyfin 12 es gate obligatorio de todo diferencial

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0112-wc-jf12-gate-obligatorio.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0112-wc-jf12-gate-obligatorio.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
EstadoLOCKED
Fecha2026-09-28
Ficherodocs/decisions/dec-0112-wc-jf12-gate-obligatorio.md

Enmienda a: r48

Por qué importa (del frontmatter del ADR):

Gobierna cuándo Styx puede afirmar un diferencial frente a Jellyfin y cómo se mide. Sin este ADR, un executor leería r48 L4 («WC-HIBR-01 NO es gate, es input de diseño») y cerraría nodos con claims de rendimiento sin benchmark, o repetiría los overclaims de jellyfin12-design (3x en direct play, RSS por sesión que esconde el coste fijo de r17, ratios contra un suelo, p99 sobre 5 muestras). Fija la metodología, los ratios por benchmark y la derivación del gate vía evidence-manifest.

Nodos del roadmap que lo citan en refs: track/media-engine, track/docs

Páginas de la documentación que lo citan: Escribir una página de contrato (especificado), Motor de medios y plan de reproducción (implementado), Motores Zig y libav (especificado), Visión (especificado)

Texto del ADR

Leído de docs/decisions/dec-0112-wc-jf12-gate-obligatorio.md, el fichero canónico.

dec-0112 — La suite WC-JF12-xx contra Jellyfin 12 es gate obligatorio de todo diferencial

  • Fecha: 2026-09-28
  • Estado: LOCKED
  • Decisor: waxin, vía AskUserQuestion (2026-09-28): la suite WC-JF12-xx (seek, densidad, plan, crash, librería, subtítulos) contra Jellyfin 12 en el mismo host es GATE OBLIGATORIO: un diferencial frente a Jellyfin sólo cuenta como hecho si el benchmark reproducible lo demuestra con el ratio fijado.
  • Enmienda: r48 L4 (WC-HIBR-01 era input de diseño, «Styx debe ganar» prohibido como gate).
  • Cita: r28 §3 (anti-especulativo: cada capa termina en un workload real), r31 (verde falso; verificador ≠ implementador), r17 (split-duro: coste fijo de la topología), r23 (capabilities versionadas), r44 (SLOs de Candidate H), dec-0103.
  • Numeración: el brief de la tanda reservaba dec-0110; la numeración final de las tandas 2/3 asigna dec-0110 al motor de medios dual, dec-0111 a client-core ABI v0 (rama w2/client-core-c0) y dec-0113 a identity token model (rama w2/identity). Este ADR queda en dec-0112. Tabla completa en la cabecera de dec-0110.

Contexto

  1. r48 L4 fijó UN benchmark, WC-HIBR-01 (4K REMUX ~80 Mbps, 2 h, fuente remota), Styx vs Jellyfin, explícitamente como input de diseño y no gate. El motivo era sano entonces: no había runtime que medir y un gate «Styx gana» habría empujado a diseñar para el benchmark.
  2. El análisis de Jellyfin 12 (jellyfin12-design, sesión 2026-09-28) propone una familia WC-JF12-xx con objetivos «3x». Su crítica adversarial encontró que varios ratios parten de premisas falsas o no demuestran nada:
    • en direct play Jellyfin ya sirve el fichero por HTTP con range requests, sin FFmpeg: un 1/3 de TTFF ahí lo dominan el buffer del cliente y el RTT, no el servidor;
    • Chromium no puede hacer direct play de un REMUX MKV HEVC Main10 + TrueHD (MSE no admite Matroska; el audio no es reproducible): «0 transcodes» es imposible en ese cliente;
    • la RSS por sesión esconde el coste fijo de r17 (7 servicios + daemon + Postgres + Valkey + NATS + otel-collector frente a un proceso con SQLite);
    • ratios contra un suelo (0 stalls frente a 0, TTFF de ~300 ms) no significan nada;
    • p99 sobre 5 corridas no tiene sentido estadístico;
    • 10 % de pérdida a 80 Mbps no lo sostiene ningún transporte: ratio arbitrario;
    • el kill del «proceso de datos» no es simétrico (Jellyfin no tiene HA).
  3. waxin decide que la comparación deja de ser input y pasa a ser gate: lo que no esté medido así no se afirma. El riesgo que r48 L4 evitaba («diseñar para el benchmark») se neutraliza con pre-registro de objetivos, métricas simétricas y separación por modo (§2).

Qué se decide

1. Regla de gate

  • Un diferencial frente a Jellyfin (cualquier afirmación «más rápido / más denso / menos transcodes / más robusto / mejor identificación que Jellyfin») sólo cuenta como hecho si un item WC-JF12-* con verdict: pass lo respalda en el evidence-manifest del nodo que lo afirma.
  • Un nodo cuyo objetivo o definición de hecho incluya un diferencial no puede declararse done sin ese item en pass. Si el benchmark no lo demuestra, el nodo tiene dos salidas, ambas explícitas y registradas: (a) seguir abierto, o (b) retirar o rebajar el claim (p. ej. de «ratio 1/3» a «paridad») vía AskUserQuestion. Rebajar en silencio está prohibido.
  • Prohibido publicar en docs, README, product-horizon o UI un «Nx mejor que Jellyfin» sin item en pass. Tampoco se afirma nada frente a Plex u otros no medidos en el harness.
  • Lo que r48 L4 prohibía («Styx debe ganar» como gate genérico) sigue prohibido en esa forma: el gate no es «ganar», es «el claim concreto que haces se sostiene con el ratio pre-registrado».

2. Metodología (común a toda la suite)

Corpus fijado por hash.

  • corpus.manifest con sha256 por fichero, tamaño, origen/licencia y, para lo sintético, el generador + seed. Toda corrida registra el hash del manifest; si no coincide, la corrida no vale.
  • La librería sintética (LIB-01, SCAN-01) se genera con clips mínimos válidos (demuxables por ambos sistemas), nunca con ficheros dummy: Jellyfin necesita sondearlos y un dummy falsea la comparación.

Mismo host, contenedores lado a lado.

  • Jellyfin 12.0 oficial, fijado por digest de imagen, y el stack Styx completo (compose de deploy/), desplegados en el mismo host. El host de referencia es el nodo local de waxin; su huella (CPU, RAM, kernel, versión de cgroup, versiones de Docker, aceleración HW disponible) queda en host.fingerprint y los objetivos se pre-registran contra ese host. Evidencia generada en otro host (p. ej. el contenedor de desarrollo) no cierra items.
  • Medición alternada: sólo un sistema bajo carga cada vez; el otro stack parado (docker compose stop) para que no compita por CPU/caché. Orden intercalado A B A B … para cancelar deriva térmica y de caché de página; caché de página del host vaciada entre corridas cuando la métrica sea de arranque en frío (y declarado cuando sea en caliente).
  • Jellyfin bien configurado: aceleración HW activa si el host la tiene (y la misma para Styx), proveedores de metadata remotos desactivados en ambos lados (o cacheados igual), medición en caliente tras la migración de BD de la 12.0, versión de jellyfin-web 12.0 fijada. Se compara contra su mejor configuración razonable, no contra la de fábrica.
  • Mismos perfiles de cliente a ambos lados: cada perfil existe a la vez como DeviceProfile de Jellyfin y como capabilities r23 de Styx, generados desde una única fuente en el harness.

Separación por modo (corrección de la crítica, punto 1). Cada benchmark de reproducción se reporta por separado en direct play / remux / transcode. Los ratios agresivos sólo se pre-registran donde Jellyfin usa FFmpeg de verdad (remux y transcode). En direct play el objetivo por defecto es paridad.

Clientes. Cliente sintético sin interfaz (pull de byte ranges / segmentos, registra stalls y tiempos por evento) para seek y densidad; perfil «direct-play-capable» (Apple/TV emulado por capabilities) para el escenario WC-HIBR-01; Chromium con MSE sólo para remux y UX.

Red. Rejilla netem realista: pérdida {0,5 %, 1 %, 2 %} × RTT {20, 50, 80 ms}, más el caso sin netem. No se usa 10 % de pérdida.

Corridas y estadística.

  • 5 corridas por sistema y escenario (mínimo).
  • p50/p95/p99 se calculan sobre muestras por evento agregadas (cada seek, cada request, cada primer frame), no sobre medias por corrida. p99 sólo se reporta con n ≥ 1000 eventos; por debajo se reporta «n/a — muestras insuficientes» y el item no puede apoyarse en p99.
  • IC95 por bootstrap remuestreando corridas. Un ratio pasa sólo si el IC95 del ratio entero queda del lado del objetivo, no sólo el punto.
  • Suelo por métrica: si el baseline de Jellyfin está por debajo del suelo declarado (por defecto: < 3 × la dispersión entre corridas (MAD), o < 50 ms en latencias, o < 9 eventos en conteos), el ratio no aplica y la métrica se evalúa como umbral (Styx ≤ Jellyfin + IC95).

Recursos: host completo, simétrico (corrección de la crítica, punto 4).

  • CPU y memoria por cgroup v2 de cada contenedor (cpu.stat usage_usec, memory.current, memory.peak), muestreados a 1 Hz, agregados por sistema: Jellyfin = su contenedor (con sus hijos ffmpeg); Styx = todos sus contenedores (servicios, daemon, Postgres, Valkey, NATS, otel-collector).
  • Se reportan siempre: RSS de host en reposo, RSS con N sesiones, pendiente marginal por sesión, CPU-segundos por Gbps servido y el punto de corte N* donde Styx pasa a costar menos que Jellyfin. Nunca «RSS por sesión» sola.

Pre-registro. El baseline de Jellyfin se publica antes que los números de Styx. Los objetivos (targets.yml: métrica, clase ratio|umbral|paridad, objetivo, suelo, modo) se commitean antes de la corrida de Styx; cambiarlos después de ver datos de Styx exige AskUserQuestion.

3. Suite y objetivos pre-registrados (v1)

Clase: R = ratio Styx/Jellyfin (con IC95 y suelo), U = umbral absoluto, P = paridad (Styx ≤ Jellyfin + IC95), I = informe obligatorio sin objetivo.

ItemQué mideObjetivos
WC-JF12-SEEK-01TTFF y seek aleatorio (50 seeks/corrida, incl. saltos > 30 min) sobre el REMUX 4K; absorbe el escenario WC-HIBR-01remux: seek p95 R ≤ 1/3, TTFF p95 R ≤ 1/3, stalls/h U = 0 · direct play: TTFF y seek p95 P, stalls/h U = 0 · bytes-before-first-frame y overfetch I
WC-JF12-DENSITY-01sesiones sostenidas sin stall en rampa 1→N hasta saturaciónremux: sesiones por núcleo R ≥ 3x, CPU/Gbps R ≤ 1/3, pendiente RSS marginal R ≤ 1/3 · direct play: sesiones por núcleo P · RSS en reposo de host y N* I
WC-JF12-PLAN-01tasa de transcode completo en matriz 30 ficheros × 6 perfiles (mismos perfiles en ambos lados)tasa de transcode completo R ≤ 1/3 (si Jellyfin < 9/180: U Styx ≤ Jellyfin) · planes con explicación máquina-legible por pista U = 100 %
WC-JF12-CRASH-0120 kills aleatorios en 2 h, dos escenarios simétricos: (a) proceso de datos (daemon Styx vs hijo ffmpeg de Jellyfin), (b) proceso servidor(a) pausa visible p95 U < 1,5 s y R ≤ 1/3 si el baseline supera el suelo; segmentos perdidos U = 0 · (b) reanudación sin acción del usuario U (Styx) e I (Jellyfin)
WC-JF12-LIB-01Continue Watching, Next Up, búsqueda y listado sobre 50k items / 200k episodiosp99 de cada endpoint R ≤ 1/3 (baseline de la 12.0 primero: es el área con BD rediseñada, la más difícil)
WC-JF12-SUBS-0140 ficheros con SRT/ASS/PGS/VobSub y embebidos grandesbloqueos de vídeo causados por subtítulos U = 0 · tiempo al primer cue (texto) p95 R ≤ 1/3 · subtítulos de imagen: ruta declarada (render en cliente o burn-in contado como transcode, reportado aparte) I
  • WC-JF12-SCAN-01 (escaneo e identificación) entra en la suite con las mismas reglas cuando exista su consumidor; sus objetivos los fija dec-0114.
  • Ampliar la suite (p. ej. LIVE-01 zapping/EPG, UPGRADE-01 migración reversible, UX-01, REMOTE-01) es añadir un item con sus objetivos pre-registrados vía AskUserQuestion; no hace falta enmendar este ADR.
  • Transcode por HW y AV2: sin ratio en v1 (no hay encoder AV2 de referencia comparable en FFmpeg 8.x). Cuando se mida, primero P de tasa de éxito por perfil.

4. Cómo se deriva el gate (evidence-manifest, GUARD 6)

  1. El harness (lo prepara la lane w3prep/jf12-harness; ruta propuesta tools/jf12-bench/, gana la ruta real) produce por corrida un results.json con: muestras crudas por evento, resúmenes por corrida, series cgroup, host.fingerprint, digests de imagen, hash de corpus.manifest, commit de Styx, configuración de motor (dec-0110: engine=zig|libav) y netem aplicado.
  2. Un paso verdict mecánico (sin juicio humano) lee results.json + targets.yml y emite por item: pass | fail | inconclusive (inconclusive = IC95 cruza el objetivo, n insuficiente o huella de host/corpus que no coincide), con el cálculo resumido.
  3. Ese resultado se transcribe a docs/<nodo>/evidence/gate.manifest.yml con command, output (resumen del verdict: ratio, IC95, n, modo), evidenceRef (resultados crudos commiteados, o ruta + sha256 si pesan demasiado), author ≠ verifier (r31).
  4. El item del modelo lleva class: benchmark-jf12 y el bloque gate: del nodo es generated: true: el veredicto del modelo se deriva del manifest, nunca se escribe a mano.
  5. Dónde cuelga cada item (propuesta; se materializa en el modelo en la pasada de gobernanza, no aquí): SEEK-01 y DENSITY-01 → nodo del motor de medios / track/byte-runtime; PLAN-01, CRASH-01 y SUBS-01 → outcome/playback-core; LIB-01 → track/domain-media / outcome/first-vertical; SCAN-01 → la wave de scanner de track/plugin-seams (dec-0114). La autoridad de metodología sigue en spike/reference-harvest (split de r48 L4 intacto): metodología, baseline y targets.yml allí; decisiones de runtime en cada nodo.

Alternativas rechazadas

  • Mantener r48 L4 (input de diseño, no gate). Rechazada por waxin: deja que los diferenciales se afirmen sin prueba; es exactamente el patrón de overclaim que la crítica encontró.
  • Gate genérico «Styx gana a Jellyfin». Rechazada (y seguía prohibida por r48): sin modo, sin pre-registro y sin métricas simétricas es manipulable en ambos sentidos.
  • 3x uniforme en todo. Rechazada: en direct play el servidor no es el cuello de botella; con baselines en el suelo el ratio es ruido. Se sustituye por clases R/U/P/I por métrica.
  • RSS por sesión como métrica de densidad. Rechazada: esconde el coste fijo de r17. Se exige host completo por cgroup, reposo, pendiente marginal y punto de corte.
  • Medir en CI / contenedor de desarrollo. Rechazada: host no controlado, sin aceleración HW declarada, ruido de vecinos. El host de referencia es el nodo local de waxin.
  • p99 por corrida con 5 corridas. Rechazada: estadísticamente vacío. Muestreo por evento.

Consecuencias

  • track/byte-runtime M3 (WC-HIBR-01) deja de ser un informe y pasa a ser el escenario direct play / remux de SEEK-01; su DoD («no convertir esto en gate») queda superado por este ADR.
  • Los nodos con claims de diferencial necesitan un item WC-JF12 en su gate antes de cerrar. Los que no los afirmen no se ven afectados.
  • El harness y el corpus pasan a ser infraestructura de gate: su propio código entra bajo testing-evidence (mutante del verdict que invierte un resultado debe fallar).
  • Coste asumido: correr la suite es caro (horas por campaña, en el nodo local). Se corre por campaña cuando un nodo quiere cerrar un claim, no en cada commit.

Lo que este ADR NO decide

  • No crea nodos, milestones ni items en styx.model.yml ni toca ROADMAP.md: eso es la pasada de gobernanza posterior.
  • No ejecuta la suite. El harness se prepara en el repo; la ejecución ocurre en el nodo local de waxin.
  • No elige el cliente direct-play físico (Apple/TV concreto); v1 usa el perfil emulado y el cliente sintético. Un dispositivo real se añade como escenario extra.
  • No fija los valores de targets.yml más allá de la tabla §3; los suelos concretos por métrica se pre-registran con el baseline de Jellyfin.
  • No decide la ruta final del harness ni su lenguaje.
  • No incluye Plex, Emby ni Lunarr (la «full campaign» de r48 L4 sigue diferida).

Back-refs

  • r48 L4 lleva banner de enmienda apuntando aquí.
  • Relacionados de la misma sesión de locks: dec-0110 (motor de medios dual: la configuración engine es una dimensión de las corridas), dec-0114 (scanner por contenido: SCAN-01).