dec-0057

Vista generada de dec-0057: r43 — D5-01: migrar a zig 0.17 en vez de vendorizar el dev build muerto

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0057-zig-017-migration-over-vendorize.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0057-zig-017-migration-over-vendorize.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-07-02
Referencia legadar43
Ficherodocs/decisions/dec-0057-zig-017-migration-over-vendorize.md

Nodos del roadmap que lo citan en refs: ninguno.

Páginas de la documentación que lo citan: ninguna todavía.

Texto del ADR

Leído de docs/decisions/dec-0057-zig-017-migration-over-vendorize.md, el fichero canónico.

r43 — D5-01: migrar a zig 0.17 en vez de vendorizar el dev build muerto

Contexto — el problema D5-01

El data plane Zig (native/zig/media-daemon/ → quic-zig → libxev) sólo compilaba junto contra zig 0.16.0-dev.3133+5ec8e45f3, que zig purgó del upstream (404 en ziglang.org). Stable 0.16.0 rompe el daemon; master 0.17-dev rompe las deps. Un docker build que resuelva zig del upstream aborta → M2.F.4 (dockerizar) bloqueado, y el zig job del CI degradado a informativo (continue-on-error, no hard-gate) desde el episodio 2026-06-27.

El brief docs/handoffs/D5-01-zig-toolchain-decision-brief.md recomendó Opción A — vendorizar el dev.3133 como GitHub Release asset, y estimó Opción B (migrar/forkear) en "semanas". waxin cuestionó el band-aid: "no podemos actualizar al 17? o algo? o buscar mejor?" — y pidió diligencia real antes de lockear.

Diligencia (evidencia, no estimates de brief)

3 Explore agents en paralelo + un build REAL con zig 0.17.0-dev.978 (que waxin ya tenía local vía zv) + micro-tests. Hallazgos verificados:

  1. Daemon source = TRIVIAL en 0.17 (0-1 cambios). Ya fuertemente adaptado a 0.16; StringArrayHashMap / AutoHashMapUnmanaged / ArrayListUnmanaged.empty / DebugAllocator / std.Thread.spawn / std.fmt siguen vivos en 0.17 (verificado vs master GitHub). No usa std.Io/std.fs (usa std.c FFI directo) → esquiva el churn del Writergate. Único posible ajuste: @bitCast(std.posix.O) (semántica "logical bit layout" 0.17), transparente en little-endian.
  2. @ptrCast/@intCast/@bitCast single-arg = FINE en 0.17. El agente de quic-zig afirmó una migración de "300+ sitios @ptrCast" citando "precedente 0.12→0.13" → REFUTADO compilando un test real con 0.17 (.o generado). Era confusión con una migración VIEJA (0.11→0.12); el single-arg result-location es la forma actual desde 0.12. Esa claim inflaba el estimate ~3-4h.
  3. Blockers REALES (confirmados por el build real, no especulación): 2 patches mecánicos de Build API en las deps:
    • docs/references/quic-zig/build.zig: b.args removido en 0.17 (~13 sitios en run-steps de dev tooling — interop clients/benchmarks, no la librería que importa el daemon).
    • libxev/build.zig: findProgram() cambió de firma (2-arg → 1-arg).
  4. addModule/createModule/b.path = probablemente fine: el build.zig del daemon usa las mismas APIs y pasó en el build real (el primer error fue en la build.zig de la DEP, no del daemon).
  5. Upstream congelado: endel/quic-zig HEAD == el pin 344c0e4 (0 commits ahead, migración "Phase 2 in-progress" detenida). mitchellh/libxev pin a82a04e ya es prácticamente HEAD (2 commits triviales después) y no soporta 0.17 aún. → ambos requieren fork/patch local.

Veredicto de esfuerzo (calibrado LLM executor, ÷8 vs humano): MEDIO (~2-5h), NO DURO.

Decisión

Lock D5-01 = Opción B — migrar el data plane a zig 0.17. Se rechaza vendorizar el dev.3133 muerto como lock (queda como fallback documentado).

Alcance del lock:

  • Fork MKS2508/quic-zig (extendiendo r36, que ya prevé esta fork para el leak fix c4c9069): patchear build.zig (b.args → nueva API de run-args) + bump minimum_zig_version a 0.17.0. El fork ahora carga DOS cosas: el leak fix P1 (r36) + los patches de Build API 0.17.
  • Fork/patch libxev (pin maxed, upstream no ha migrado): findProgram 1-arg + cualquier shim residual de std.posix que 0.17 rompa. Preferir PR upstream a mitchellh; fork local si tarda.
  • .gitmodules / build.zig.zon: apuntar a los forks + pin a un 0.17-dev vivo (0.17.0-dev.1158+1d1193aa7, disponible en ziglang.org hoy).
  • Daemon: 0-1 cambios (bump minimum_zig; posible ajuste @bitCast si el build lo pide).
  • CI (guard.yml zig job): pasa de continue-on-error informativo a hard-gate (resuelve 0.17-dev del index oficial, que ahora compila).
  • M2.F.4 dockerize: Dockerfile sobre el toolchain 0.17 vivo.

Razones (una línea cada una)

  1. Esfuerzo verificado MEDIO, no "semanas" — el brief usó estimate humano + una claim alucinada (@ptrCast) que inflaba a DURO. La diligencia lo corrige con evidencia real.
  2. Frontier (r16) — ante dos opciones plausibles, priorizar la que aumenta control e2e / modernidad, salvo blocker material. No hay blocker material: la migración es mecánica.
  3. El "vendorizar" hace falta IGUAL para evitar el purge de dev builds (0.17-dev también se purga). La pregunta real no era migrar-vs-vendorizar, sino qué build pinnear: B pinnea un 0.17-dev VIVO; A pinnea un 0.16-dev MUERTO. Vendorizar un build vivo-pero-purgable de 0.17 es un follow-up trivial si hace falta, sobre una base moderna.
  4. Desbloquea el CI zig-job como hard-gate — con A el zig-job seguiría informativo (0.16-dev.3133 no está en el index oficial); con B resuelve 0.17-dev del index y vuelve a bloquear PRs.
  5. Sale de la dependencia de un build muerto — deuda técnica estructural (403M vendorizados de un zig que nadie más usa) eliminada.

Consecuencias / trade-offs honestos

  • Mantenimiento de 2 forks (quic-zig + libxev) sincronizados con 0.17-dev mientras 0.17 sea dev (moving target). Al estabilizar 0.17, bump a stable = punto permanente.
  • Riesgo residual acotado: la profundidad exacta de la migración de las build.zig de las deps más allá de los 2 errores confirmados es mecánica pero no 100% enumerada. El handoff arranca por un spike de build iterativo (patch → rebuild → siguiente error) para cuantificar antes del fork formal. Si aparece un blocker DURO real (unknown-unknown), reabrir esta decisión (fallback A sigue disponible).
  • No congela M2.F.1/.2/.3 (ya mergeados + smoke GREEN): la migración no toca la lógica del daemon, sólo el toolchain + build.zig de las deps.

Ejecución

NO autónoma por axon. Handoff → task-executor: docs/handoffs/next-prompt-d5-01-zig-017-migration.md. axon verifica (build 0.17 verde + 146/146 tests + smoke e2e re-run + CI zig-job hard-gate green) antes de cerrar.


Addendum 2026-07-02 — spike M0 ejecutado + diligencia adicional (decisión B se mantiene, scope corregido)

El spike M0 del handoff corrió (task-executor a406db9e + verificación independiente axon). La decisión B (migrar a 0.17) se mantiene, pero la diligencia original no escaneó el source de quic-zig más allá de su build.zig y erró el scope. Correcciones al record:

Hallazgo no anticipado — el operador ** fue eliminado del lenguaje en 0.17

** (repetición de arrays/tuplas, ej. [_]u8{0} ** 12) fue removido del grammar de Zig 0.17, reemplazado por @splat. Verificado en tres capas (tokenizer sin asterisk_asterisk, Parse.zig sin array_mult en la tabla de precedencia, AST sin nodo .array_mult) contra dos fuentes independientes:

  • El install local 0.17.0-dev.978+a078d55a2 (vía zv): [_]u8{0} ** 8 → error: binary operator '*' has whitespace on one side; @splat(0) da bytes idénticos.
  • Codeberg master (codeberg.org/ziglang/zig, el trayecto VIVO de Zig): confirma la remoción.

⚠️ Nota infra crítica: github.com/ziglang/zig es un mirror CONGELADO (HEAD == "README: migrated to codeberg") y todavía muestra ** fósil. No grepear github para estado actual de Zig. Referencia local canónica = clone de codeberg en ../zig-lang (sibling de styx, live master).

Scope real (verificado por grep + build iterativo)

Componenter43 originalRealidad verificada
Daemon source (media-daemon+media-core)trivial✅ 0 sitios **-repeat (los 55 hits de grep crudo son markdown-bold en doc-comments)
Build.zig de deps2 patches4 — quic-zig b.args→addPassthruArgs (13 sitios) + libxev findProgram 1-arg + libxev install_prefix removido (idiom pkg-config relocatable) + libxev build_root→root (×3). Todos mecánicos, fixeados en el working-tree del spike
quic-zig sourceno escaneado~160 sitios **→@splat (36 tls13.zig, 35 connection.zig, 28 crypto.zig — el core del handshake)

Re-veredicto de esfuerzo

Mecánicamente sigue MEDIO (~1-1.5h LLM): @splat es reemplazo 1:1 (mismos bytes, cero cambio de runtime), verificado en sandbox para todas las formas (byte escalar, char, struct-literal, longitud por const, default de campo). Edge cases acotados: 3 string-repeat ("x" ** N → cambio de tipo + & en call-site) + 1 multi-elemento en un test{} de libxev (comptime loop). NO es DURO, pero el SCOPE es más ancho de lo que r43 vendió: parchea source TLS/QUIC de la dep, no solo su build.zig. Fallback A (vendorizar el 0.16-dev.3133 muerto) sigue disponible per este ADR si el scope creciera.

Mecanismo de carga refinado (lock waxin 2026-07-02)

r43 asumió "fork GitHub". Refinado tras confirmar el terreno:

  • quic-zig → fork EXISTENTE MKS2508/quic-zig (fork de endel/quic-zig, 8 ramas vivas). Ya carga el leak fix r36 como PR #29 (pr/disposal-on-ack-lifecycle) + issue #30, ambos OPEN sin respuesta upstream. La migración 0.17 (@splat + 4 build.zig) se apila en una rama nueva encima → cero carga nueva (la fork ya existe; era la incomodidad de waxin — resuelta: reusar ≠ crear). NO vendor-in-tree.
  • libxev → upstream-first: mitchellh/libxev no tiene issue/PR de 0.17 (seríamos los primeros). Abrir issue + PR con los 3 deltas de build.zig; fork MKS2508/libxev solo si no responden.
  • issue #25 (endel/quic-zig) — "PTO does not rescue unidirectional send streams". Bug de corrección relevante para Styx (exposure = WebTransport/bare-QUIC empujando bulk data por uni streams = MoQ media delivery). Se arregla en la fork como commit atómico SEPARADO, NO bundle con la migración toolchain (mantiene la migración verificable-limpia). PR upstream separado (o stacked), no mezclado con #29.

Referencia infra añadida

Clone shallow de codeberg ziglang/zig → ../zig-lang (sibling de styx, fuera del tree). Fuente de verdad local para verificación de APIs/grammar de Zig — evita round-trips web y el mirror github stale.

Addendum (Lane B session, 2026-07-05) — libxev fork .decls gap closed

Drift detectado durante M3 verification (Lane B, commit 9da4452): el spec note v36 schemaNote documentó .decls→std.meta.declarations como hecho, pero la migración quedó incompleta en el libxev fork (MKS2508/libxev@2db65bf, rama feat/zig-0.17). 6 sitios con .decls (API removida en zig 0.17 std.Type.Struct) sobrevivieron al push inicial:

ArchivoLíneaContexto
src/main.zig47comptime validation block en xev
src/watcher/tcp.zig523test "TCP: Stream decls"
src/watcher/file.zig638test "File: Stream decls"
src/watcher/udp.zig918test "UDP: Stream decls"
src/watcher/stream.zig148comptime de Pollable
src/watcher/stream.zig1136test "Stream decls"

Symptom: zig build test (test-only) pasaba 146/146 pero zig build (install binaries styx-media-daemon + styx-wt-client) fallaba con "no field named 'decls' in struct 'lang.Type.Struct'". Smoke D5-01 M5 funcionó porque usó binario pre-built (Jul 2), no rebuild fresco.

Fix ejecutado en fork (workflow multi-repo, 3 pushes):

  • MKS2508/libxev@7f067af — .decls→.decl_names (zig 0.17 std.Type.Struct API change). Loop variable cambia tipo de Declaration struct (con .name) a [:0]const u8 (string directo); refs downstream decl.name → solo decl. Mismo patrón canónico que styx ya tenía migrado en docs/references/quic-zig/src/event_loop.zig:261,1359.
  • MKS2508/quic-zig@e1a034e — submodule bump: build.zig.zon .libxev.url SHA 99e0031 → 7f067af + nuevo content hash libxev-0.0.0-86vtc6gUFADXrA3PX99k2xjY0Nsyf6qx08pmEUOO4G6S.
  • styx@7776291 — submodule pin bump registrado en el repo parent.

Verificación: zig build exit 0 (binarios styx-media-daemon 6.7M + styx-wt-client 5.2M), zig build test 13/13 + 146/146, bun run check:roadmap 28/28.

Implicación doctrinal: la lista exhaustiva de olas migradas en r43 (→@splat (159), .decls→std.meta.declarations, params→param_types, std.meta.Tuple removido, etc) era correcta en styx-owned code + quic-zig fork, pero incompleta en libxev fork. Doctrina "upstream-first → fork mientras tanto" sigue válida; lo que faltó fue un sweep comprehensivo del fork en push-time. Fix de proceso para futuras migraciones: pre-push grep exhaustivo de todos los símbolos documentados como migrados contra el fork entero (no solo el path principal del código migrado). Aplicar cuando migración similar toque forks adicionales.

Ref: docs/handoffs/archive/M2-integration-report.md §8.