paso 8 bis: pigz sobre zlib-rs, sellado — el swap en un consumidor real no costó una línea de su fuente
La lib andando contra un programa de prueba propio no decide una campaña de 122 recetas. pigz sí, y se eligió por duro: no usa compress2 sino los rincones de zlib —inflateBack, inflateBackInit_, inflateBackEnd, deflatePending, deflatePrime, crc32_combine, deflateSetHeader—, los siete comprobados en los símbolos de zlib-rs ANTES de intentar el build. Y viaja en 6 perfiles. b3:3a485edc… sellada. Lo único distinto de recipes/pigz.toml: la dep y `LIBS="… -lz -lunwind"`. El -lunwind va en LIBS y no en LDFLAGS porque el Makefile pone $(LDFLAGS) antes de los objetos y $(LIBS) al final: en enlace estático el orden decide. MEDIDO (Silesia 15,7 MB, un hilo): comprimir -6 canónico 6.410.662 B / 0,726 s · zlib-rs 6.455.201 B / 0,326 s (+0,69 %, 2,23×) descomprimir canónico 0,065 s · zlib-rs 0,059 s binario 1.062.672 B · 3.971.304 B (+2,9 MB, ×3,7) INTEROPERABILIDAD en las tres direcciones: el canónico lee el .gz de zlib-rs, zlib-rs lee el del canónico, y el gzip del corpus —tercera implementación— lee los dos, recuperando el sha256 original. De las tres reservas del paso 8 quedan dos (no es byte-idéntica; gzprintf no existe) y aparece una tercera: +2,9 MB por binario no es gratis en una imagen con muchos consumidores de zlib. yupana gana el sufijo `-zlibrs` en SUFIJOS_VARIANTE: sin eso el invariante (c) marcaría pigz/pigz-zlibrs como colisión-fuente ACCIONABLE, un falso positivo. Comprobado: duplicados sigue en «ninguna».
Semántico sayk'uy
Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.
3 qillqakuna · +103 −1
- docs/plan-que-sigue-tras-busybox.md +51 −0
- recipes/pigz-zlibrs.toml +46 −0
- scripts/yupana.py +6 −1
diff --git a/docs/plan-que-sigue-tras-busybox.md b/docs/plan-que-sigue-tras-busybox.md
index 9737b880..7c2812f3 100644
--- a/docs/plan-que-sigue-tras-busybox.md
+++ b/docs/plan-que-sigue-tras-busybox.md
@@ -1002,3 +1002,54 @@ usa, pero el store lo paga.
que el plan suponía. La receta queda sellada en el catálogo con 0 consumidores y en ningún perfil: el
siguiente paso natural es swapear UN consumidor real (`lld21` pide `libz.so.1`; `pigz` ejercita
deflate a fondo) y correr su prueba, ahora que el artefacto existe y el enlace está caracterizado.
+
+## Paso 8 bis — el swap en un consumidor REAL: `pigz` sobre `zlib-rs`, sellado y verificado
+
+La lib andando contra un programa de prueba propio no decide una campaña de 122 recetas. Lo que
+decide es si un consumidor **de verdad**, con su Makefile y su uso de la API, compila y corre **sin
+tocar su fuente**. `recipes/pigz-zlibrs.toml`: `b3:3a485edc…`, sellada.
+
+**pigz es el consumidor duro, y por eso se eligió**: no usa el `compress2` de manual sino los rincones
+de zlib — `inflateBack`, `inflateBackInit_`, `inflateBackEnd`, `deflatePending`, `deflatePrime`,
+`crc32_combine`, `deflateSetHeader`. Comprobado que zlib-rs exporta **los siete** antes de intentar el
+build. Y viaja en 6 perfiles.
+
+**Lo único que cambió respecto de `recipes/pigz.toml`:** la dep (`zlib-rs` en vez de `zlib`) y
+`LIBS="… -lz -lunwind"`. El `-lunwind` va en `LIBS` y NO en `LDFLAGS` porque el Makefile de pigz pone
+`$(LDFLAGS)` antes de los objetos y `$(LIBS)` al final (línea 15): en enlace estático el orden decide
+y el unwinder tiene que ir DESPUÉS de quien lo pide. Fuente, banderas y fases, idénticas — la
+comparación vale porque lo único distinto es la lib.
+
+### Medido, con el corpus Silesia (15,7 MB) y un solo hilo para que el tiempo sea comparable
+
+| | `pigz` (zlib 1.3.1) | `pigz-zlibrs` |
+|---|---|---|
+| comprimir `-6` | 6.410.662 B · **0,726 s** | 6.455.201 B · **0,326 s** |
+| descomprimir (ejercita `inflateBack`) | 0,065 s | **0,059 s** |
+| tamaño del binario | 1.062.672 B | **3.971.304 B** |
+
+⇒ **+0,69 % de tamaño comprimido, 2,23× más rápido comprimiendo**, y descompresión un pelo más
+rápida. El precio está en el binario: **+2,9 MB** (×3,7), que es el mundo de Rust que el `staticlib`
+arrastra.
+
+### Interoperabilidad, en las tres direcciones que importan
+
+```
+canónico lee el .gz de zlib-rs ⇒ OK (sha256 del original recuperado)
+zlib-rs lee el .gz del canónico ⇒ OK
+el `gzip` del corpus lee los dos ⇒ OK ← tercera implementación, independiente
+```
+
+O sea: el formato es el mismo y el CRC cierra. Lo que difiere es **qué bytes elige el compresor**, que
+es la reserva nº 1 del paso 8 y sigue en pie.
+
+### Lo que esto cambia en el veredicto
+
+El swap en un consumidor real **no costó ni una línea de su fuente**, sólo la bandera del unwinder. Con
+eso, de las tres reservas del paso 8 quedan dos: la salida no es byte-idéntica (decisión de contenido)
+y `gzprintf` no existe (hay que mirar receta por receta). El tamaño del binario se suma como tercera:
+para una imagen con muchos consumidores de zlib, +2,9 MB por binario no es gratis.
+
+⚠ `scripts/yupana.py` gana el sufijo `-zlibrs` en `SUFIJOS_VARIANTE`: sin eso el invariante (c) marca
+`pigz`/`pigz-zlibrs` como «colisión-fuente ACCIONABLE», que es un falso positivo — y un guardián con
+falsos positivos se empieza a ignorar. Comprobado: `yupana duplicados` sigue en «ninguna».
diff --git a/recipes/pigz-zlibrs.toml b/recipes/pigz-zlibrs.toml
new file mode 100644
index 00000000..4caf1d04
--- /dev/null
+++ b/recipes/pigz-zlibrs.toml
@@ -0,0 +1,46 @@
+# pigz 2.8 sobre zlib-rs — la PRUEBA DE CONSUMIDOR del paso 8 del plan, sellada.
+#
+# ── POR QUÉ ESTA VARIANTE EXISTE ────────────────────────────────────────────────────────────────
+# `recipes/zlib-rs.toml` demostró que la lib anda contra un programa de prueba propio. Eso no alcanza
+# para decidir la campaña de las 122 recetas: lo que hay que saber es si un **consumidor de verdad**,
+# con su propio Makefile y su propio uso de la API, compila y CORRE sin tocar su fuente.
+#
+# pigz es el mejor candidato del corpus para eso, y no por ser chico:
+# · usa los RINCONES de zlib, no el `compress2` de manual — `inflateBack`, `inflateBackInit_`,
+# `inflateBackEnd`, `deflatePending`, `deflatePrime`, `crc32_combine`, `deflateSetHeader`.
+# Comprobado que zlib-rs exporta los siete antes de intentar el build.
+# · su producto es un fichero .gz que se puede verificar contra el `pigz` canónico: mismo formato,
+# descomprimible por el otro, y con el CRC que el formato lleva adentro.
+# · viaja en 6 perfiles, así que si el swap anda acá, anda donde importa.
+#
+# ── LO ÚNICO QUE CAMBIA RESPECTO DE `recipes/pigz.toml` ─────────────────────────────────────────
+# 1. `deps.build`: `zlib-rs` en vez de `zlib`. Publica `libz.a` con el mismo nombre, así que el
+# `-lz` del Makefile lo encuentra sin tocar nada.
+# 2. `LIBS` gana `-lunwind`. Es la reserva nº 3 del paso 8: un `staticlib` de Rust arrastra el
+# backtrace de std y sin el unwinder el enlace muere con `undefined symbol: _Unwind_Resume`.
+# Va en `LIBS` y NO en `LDFLAGS` porque el Makefile de pigz pone `$(LDFLAGS)` ANTES de los
+# objetos y `$(LIBS)` al final (línea 15) — en enlace estático el orden decide, y `-lunwind`
+# tiene que ir DESPUÉS de `-lz`, que es quien lo necesita.
+#
+# La fuente, las banderas y el resto de las fases son idénticas a la receta canónica: la comparación
+# vale porque lo único distinto es la lib.
+name = "pigz-zlibrs"
+version = "2.8"
+license = "Zlib"
+
+[source]
+tarball = "https://zlib.net/pigz/pigz-2.8.tar.gz"
+sha256 = "eb872b4f0e1f0ebe59c9f7bd8c506c4204893ba6a8492de31df416f0d5170fd0"
+
+[build]
+compiler = "zig-cc"
+target = "x86_64-linux-musl"
+link = "static"
+flags = []
+
+[build.phases]
+compile = 'make pigz CC="$CC" CFLAGS="-O2 -I/usr/include" LDFLAGS="-static -no-pie -L/usr/lib" LIBS="-lm -lpthread -lz -lunwind"'
+install = 'mkdir -p /out/usr/bin && cp pigz /out/usr/bin/pigz && ln -sf pigz /out/usr/bin/unpigz'
+
+[deps]
+build = ["zlib-rs", "make"]
diff --git a/scripts/yupana.py b/scripts/yupana.py
index 8bcb2deb..d4f5725c 100755
--- a/scripts/yupana.py
+++ b/scripts/yupana.py
@@ -127,7 +127,12 @@ def _deps_build(f):
# duplicado a fundir — es la diversificación coherente [[etapa-g-gui-chain-boundary]]. `-hello`/`-hola`
# son demos que vendorean la fuente de otro a propósito.
SUFIJOS_VARIANTE = ("-shared", "-static", "-libs", "-tiny", "-dev", "-headers", "-minimal",
- "-generic", "-metal-dual", "-metal", "-swrast", "-llvmpipe", "-hello", "-hola")
+ "-generic", "-metal-dual", "-metal", "-swrast", "-llvmpipe", "-hello", "-hola",
+ # `-zlibrs` (2026-09-22): el MISMO paquete construido contra `zlib-rs` en vez de
+ # `zlib`. Es la prueba de consumidor del paso 8; sin este sufijo el invariante (c)
+ # lo reportaría como «colisión-fuente ACCIONABLE», que es un falso positivo — y un
+ # guardián con falsos positivos se empieza a ignorar.
+ "-zlibrs")
def _base(n):