1# SDD 23 — Plan del re-hasheo masivo (corregido: la etapa 1 desmintió mi primera conclusión)
2
3Escrito 2026-08-08, tras la decisión del usuario de hacer «ahora, antes de seguir creciendo» el
4re-hasheo que llevaba tiempo pendiente.
5
6La campaña se había planteado como **dos** trabajos que exigen reconstruir casi todo el corpus:
7
81. `-ffile-prefix-map` global, para cerrar el «no-determinismo probable de 92 paquetes».
92. Separar la información de depuración en paquetes `-debug`.
10
11**Los dos son reales.** La primera versión de este documento decía que el primero se cancelaba
12«porque su premisa es falsa» — **me equivoqué, y la etapa 1 lo demostró en veinte minutos**. La
13corrección está en el §1.bis, y es el mejor argumento a favor de tener puertas.
14
15---
16
17## 1. Lo primero fue verificar la premisa (y me pasé de confiado — ver §1.bis)
18
19El plan heredado decía: *«92 paquetes con no-determinismo probable, todos por rutas `/src` en
20`.debug_*`; pendiente: `-ffile-prefix-map` global re-hashea ~720»*. Antes de comprometer días de
21granja, se comprobó. Tres medidas, en orden de fuerza creciente:
22
23**(a) Las rutas `/src` son CONSTANTES, no variables.** El lab bindea el árbol de fuentes en `/src`
24—literal, siempre— así que una ruta `/src/foo.c` embebida en `.debug_` es idéntica en cada rebuild y
25en cualquier máquina. No es no-determinismo: es una ruta fija que *parece* sospechosa.
26
27**(b) Barrido de 195 artefactos buscando rutas que sí variarían** (`/home/<user>/`,
28`/opt/hammer/work`, `/tmp/<aleatorio>`): **15 las tienen (≈8%)**. Pero al mirarlas una a una,
29ninguna es nuestra:
30
31| ruta hallada | qué es realmente |
32|---|---|
33| `/home/buildozer/aports/main/musl/src/musl-1.2.6` | la ruta de compilación **de Alpine**, horneada en el `musl` prebuilt que inyectamos |
34| `/tmp/apiresponse.json` | una **cadena literal** en el código fuente de `gron` |
35| `/home/jimmac/`, `/home/sam/dev/` | rutas de los autores dentro de los **assets SVG** de adwaita |
36
37Las tres son constantes que vienen de fuera y son idénticas en cada rebuild. **Buscar rutas con
38pinta de host no mide no-determinismo: mide qué escribieron terceros en sus fuentes.** Es la misma
39clase de error que la regla del `.a` no-PIC —`grep R_X86_64_32` contaba reubicaciones de
40`.debug_*`— y que el conteo de licencias con `grep -l license`, que contaba comentarios.
41
42**(c) La prueba que decide: reconstruir y comparar.** Se apartó el artefacto de `wl-clipboard`, se
43reconstruyó con las deps cacheadas y se comparó con `takana why-differs`:
44
45```
46 24 entradas idénticas · 0 divergen
47 ✓ los dos árboles son idénticos: REPRODUCE
48```
49
50⇒ Conclusión que saqué entonces: «la reproducibilidad se sostiene, el `-ffile-prefix-map` no hace
51falta». **Era falsa, y duró lo que tardó en correrse la etapa 1.** Ver §1.bis.
52
53> **La lección de método, que vale más que el ahorro:** el no-determinismo se mide **reconstruyendo y
54> comparando**, no buscando cadenas sospechosas. Un `grep` sobre binarios genera candidatos, no
55> veredictos. Y este proyecto ya tiene la herramienta del veredicto (`why-differs`); lo que faltaba
56> era usarla antes de planificar.
57>
58> ⚠️ Esto **no** prueba que las 1153 reproduzcan — sólo que UNA lo hace. Yo lo leí como si probara
59> más de lo que probaba, que es exactamente el error contra el que este mismo documento advierte
60> dos párrafos más arriba.
61
62---
63
64## 1.bis 🔴 CORRECCIÓN: sí hay no-determinismo, y el `-ffile-prefix-map` SÍ hace falta
65
66La etapa 1 (verificación amplia, `scripts/verificar-repro.sh`) se corrió sobre una muestra por clase
67de build. Resultado sobre las que pudieron reconstruirse:
68
69| receta | veredicto |
70|---|---|
71| `binutils` | ✓ REPRODUCE |
72| `appstream` | ✗ **DIVERGE** |
73| `bison` | ✗ **DIVERGE** |
74| `wl-clipboard` (§1c) | ✓ REPRODUCE |
75
76Y la causa, idéntica en las dos que divergen, dicha por `why-differs`:
77
78```
79difieren [.debug_aranges, .debug_info, .debug_pubnames, .debug_pubtypes, .debug_str]
80 — sólo info de depuración (el código ejecutable es idéntico)
81 · .debug_str sólo en B: /src/output/meson-private
82```
83
84**Dónde me equivoqué, exactamente:** el barrido de §1b buscaba rutas *del host* (`/home/<user>/`,
85`/tmp/<aleatorio>`) y no encontró ninguna nuestra — cierto. Pero el no-determinismo no venía de una
86ruta del host sino de **rutas internas al árbol de build** (`/src/output/meson-private`) que **varían
87entre corridas** aunque `/src` sea constante. Buscar la forma equivocada de ruta y no hallarla no
88prueba que no haya otra. Y una sola muestra que reproduce (§1c) no es una muestra.
89
90⇒ La lección del §1 sigue siendo válida y encima se refuerza: **el veredicto es reconstruir y
91comparar**. Sólo que esta vez el equivocado fui yo, y lo que me salvó fue haber puesto la puerta
92antes de la campaña en vez de después.
93
94### 🎁 Y de ahí sale la mejor noticia del plan
95Las dos mitades **son el mismo trabajo**. La divergencia vive ENTERA en secciones `.debug_*` y el
96código ejecutable es idéntico ⇒ **separar el debug del artefacto principal hace que el artefacto
97principal reproduzca**, sin necesidad de tocar `-ffile-prefix-map` en 720 recetas.
98
99Queda por decidir qué se hace con el paquete `-debug` en sí (su contenido seguiría siendo
100no-determinista sin `-ffile-prefix-map`), pero eso es un problema mucho más chico: afecta a un
101artefacto secundario que nadie instala por defecto, y puede resolverse después o a la vez.
102
103---
104
105## 2. Lo que sí queda: separar la información de depuración
106
107Medido sobre muestra amplia de `.so`/`.a`: **el 79% del contenido binario del store son secciones
108`.debug_*`** (qtdeclarative solo llega al 87%). Consecuencias, las dos que importan:
109
110- **~96 G de reserva de disco** — el store de ~126 G serían ~30 G sin debug.
111- **El espejo público baja de 126 G a ~30 G**, que es la diferencia entre alojar barato y caro, y
112 toca la decisión ya tomada de servirlo desde el Storage Box.
113
114Y no es sólo tamaño: separar el debug es lo que permite **entregar los símbolos a quien depura sin
115imponérselos a quien sólo usa la distro**, que es lo que hacen todas las distros serias.
116
117### El precio, dicho claro
118`strip` posterior **no sirve**: rompería la verificación bit-a-bit, porque el `ArtifactHash` es de
119ENTRADA (se calcula sin construir) y quedaría apuntando a un contenido que un rebuild ya no
120reproduce. Hay que hacerlo **dentro del build**, y eso cambia la fase `install` ⇒ **re-hashea las
121recetas afectadas**. Es el coste real y no hay atajo.
122
123### ⚠ Pero para la IMAGEN sí hay atajo, y es legítimo (2026-09-23)
124
125Lo de arriba vale para el STORE. **Un rootfs proyectado no es un artefacto**: se arma copiando y
126enlazando, no se sella, y nadie verifica su hash contra una receta. Stripear ahí no rompe nada —
127`strip` es determinista, así que la imagen sigue siendo reproducible— y **no toca ni un byte del
128store**, que conserva los símbolos para quien depura. Es exactamente lo que el propio §2 pide:
129«entregar los símbolos a quien depura sin imponérselos a quien sólo usa la distro».
130
131Medido sobre el rootfs de la variante `mirada` del instalador:
132
133| | |
134|---|---|
135| tal como se proyecta hoy | **1348 MiB** |
136| con `strip --strip-unneeded` sobre los ejecutables | **891 MiB** — un 34 % menos |
137| un binario suelto, `openssh`: `ssh` | **19,3 → 5,3 MiB**, un 72 % |
138
139La distancia entre el 34 % y el 72 % es la composición: en un escritorio buena parte de los bytes
140son datos —fuentes, iconos, traducciones— que no tienen símbolos. En `servidor`, dominado por
141binarios estáticos grandes (`rust` 364 MiB, `gitea` 219, `git` 216, `openssh` 202), el ahorro sería
142mayor. **No está medido para `servidor` y por eso no se pone un número.**
143
144⇒ Es la palanca barata para el tamaño de las descargas, y **no sustituye** al split dentro del
145build: eso sigue siendo lo que arregla los 126 G del store y del espejo.
146
147---
148
149## 3. 🧨 El hallazgo que condiciona CÓMO se hace: el entorno del lab NO está hasheado
150
151Al buscar dónde poner una flag global apareció algo que hay que decidir antes de tocar nada.
152
153`crates/hammer-build/src/sandbox.rs` fija el entorno de TODOS los builds:
154
155```
156CC=takana-zig-cc (con -mcpu=baseline) SOURCE_DATE_EPOCH=1 TZ=UTC LC_ALL=C
157AR="zig ar" CARGO_BUILD_… codegen-units=1
158```
159
160**Ninguna de esas entra en `Recipe::hash_inputs`**, que es una lista blanca de source, compiler,
161target, link, `zig_version`, patches, flags, phases y deps.
162
163⇒ **Cambiar el entorno del lab cambia lo que sale del build SIN cambiar un solo hash.** El store
164diría que todo está al día mientras los artefactos ya no corresponden a su hash. Es el fallo más
165peligroso posible en un sistema direccionado por contenido: no falla, miente.
166
167Hoy no muerde porque ese entorno no cambia nunca. Pero una campaña global es precisamente el momento
168en que cambiaría.
169
170### Las dos formas de hacerlo, y cuál propongo
171- **A — flag en el lab** (una línea en `sandbox.rs`, aplica a todo): barata de escribir y
172 **rompe el invariante** por lo de arriba. **Descartada**, salvo que se añada primero un
173 `lab_version` a `hash_inputs` (ver abajo).
174- **B — en la fase `install` de cada receta** (`strip --only-keep-debug` + `objcopy`): explícita,
175 entra al hash por diseño, re-hashea sólo lo que toca, y es auditable receta a receta.
176 **Propuesta.** El coste es editar ~1100 recetas, pero es mecánico y verificable — exactamente el
177 mismo patrón con el que hoy se poblaron 1074 licencias sin mover un hash.
178
179**Y un ticket previo, chico y de fondo:** añadir a `hash_inputs` un `lab_version` que capture el
180entorno del sandbox, con el mismo diseño que `zig_version` (**sólo entra si está fijado**, así los
181hashes actuales no se mueven). Sin eso, cualquier cambio futuro del lab vuelve a ser invisible.
182Es barato hoy y caro el día que haga falta.
183
184---
185
186## 4. El plan, por etapas y con puertas
187
188Cada etapa tiene una **puerta**: si no pasa, se para. Una campaña de días sin puertas es una forma
189elegante de romper el corpus.
190
191| # | etapa | puerta |
192|---|---|---|
193| 0 | `lab_version` en `hash_inputs` (opcional, no fijado) | los 1153 hashes actuales **no se mueven** |
194| 1 | Verificación AMPLIA de reproducibilidad: apartar y reconstruir una muestra de ~30 recetas de clases distintas (C, Rust, Go, meson, cmake) y comparar con `why-differs` | **0 divergencias**; si alguna diverge, ESO es el trabajo real y esta campaña se pospone |
195| 2 | Piloto del split de debug en **5 recetas** representativas | los `-debug` salen, el binario sigue funcionando, y el rebuild reproduce bit a bit |
196| 3 | Medir de verdad el ahorro y extrapolar — ✅ **HECHO, ver §6** | el ahorro real es **~60%**, no 79%: se replanteó |
197| 4 | Aplicar al corpus por tandas, con la granja | cada tanda: `store-gc` + `why-differs` sobre una muestra |
198| 5 | Regenerar grafos, perfiles y el espejo | los perfiles siguen cerrando (`escritorio-sway` 121/121, KDE 162/162…) |
199
200**Orden respecto de lo demás**: la etapa 1 conviene ANTES que nada, porque si la reproducibilidad
201amplia no se sostiene, todo lo demás cambia de prioridad. Y es barata: apartar artefactos y
202reconstruir con deps cacheadas, como se hizo con `wl-clipboard`.
203
204---
205
206## 5. Lo que este plan NO propone
207
208- **No** tocar `-ffile-prefix-map`: §1 lo cerró.
209- **No** strippear artefactos ya sellados: rompe la evidencia, que es el invariante del proyecto.
210- **No** meter la flag en el lab sin `lab_version`: §3.
211- **No** empezar por el corpus entero: §4 empieza por 5 recetas, y con razón.
212
213
214---
215
216## 6. Etapa 3 — el ahorro real es ~60%, no 79%, y las cifras publicadas estaban infladas
217
218El «79%» que este documento y los SDD 19/20 venían citando era **el 79% del CONTENIDO BINARIO**,
219medido sobre una muestra de `.so`/`.a`. Es cierto y es la cifra equivocada para planificar, porque
220un artefacto no es sólo binarios: trae cabeceras, datos, iconos, locales, `.pc` y documentación,
221que no encogen.
222
223**Medido con `scripts/medir-debug.sh`** (suma los tamaños reales de las secciones `.debug_*` vía
224`readelf -S` y los compara con el tamaño del árbol, sin reconstruir nada):
225
226| muestra | artefactos | volumen | debug / artefacto |
227|---|---:|---:|---:|
228| pequeña | 40 | 876 MB | 27% |
229| media (truncada a 60 ficheros/artefacto) | 100 | 2 955 MB | 38% |
230| **grande, sin truncar** | **250** | **8 874 MB** | **60%** |
231
232La varianza es alta porque unos pocos artefactos grandes dominan el total. **La muestra grande es la
233que manda** (cubre ~7% del store), y encaja con lo único que está medido de verdad —el piloto de la
234etapa 2, donde se reconstruyó y se pesó:
235
236- `bison` 6 M → 3 M = **−50%**
237- `appstream` 56 M → 18 M = **−68%**
238
239Los dos **encierran** el 60% de la muestra grande. Ésa es la validación: el modelo predice lo que el
240rebuild real produjo.
241
242### Las cifras corregidas
243Con el store en **127 G** y un ahorro de ~60%:
244
245| | se venía diciendo | medido |
246|---|---|---|
247| reserva de disco | ~96 G | **~76 G** |
248| store tras el split | ~30 G | **~51 G** |
249| espejo público | ~30 G | **~51 G** |
250
251Sigue siendo el mayor ahorro disponible en el proyecto —76 G no es poco— pero **no es lo prometido**,
252y la diferencia importa para dimensionar el alojamiento del espejo, que es una decisión con factura.
253
254> **La lección:** una cifra medida sobre una cosa (contenido binario) se citó durante semanas como si
255> midiera otra (tamaño de artefacto), y llegó a tres documentos. El número no era falso; **la unidad
256> sí**. Cuando un número vaya a decidir un gasto, conviene volver a la medición original y comprobar
257> QUÉ estaba midiendo.
258
259
260---
261
262## 7. 🔴 SEGUNDA CORRECCIÓN: lo que medía el test era DERIVA, no no-determinismo
263
264El §1.bis dijo «sí hay no-determinismo» al ver que `appstream` y `bison` divergían. **También estaba
265mal**, y por un fallo de diseño del propio test.
266
267`scripts/verificar-repro.sh` aparta el artefacto **guardado en el store** y lo compara con una
268reconstrucción de hoy. Pero ese artefacto guardado puede tener meses: se construyó con **otro estado
269del lab** (otro zig, otras flags, otro entorno). Así que el test conflaba dos cosas muy distintas:
270
271- **no-determinismo** — mismas entradas, mismo lab, salidas distintas. Es lo que rompe el invariante.
272- **deriva** — el artefacto viejo no es lo que el lab de HOY produce. No es no-determinismo: es que
273 el mundo se movió.
274
275**La prueba que las separa: construir DOS VECES HOY.** Y se hizo, sin querer al principio: al volver
276a correr el verificador sobre recetas ya reconstruidas, todas pasaron a REPRODUCIR.
277
278| receta | 1ª corrida (viejo vs hoy) | 2ª corrida (hoy vs hoy) |
279|---|---|---|
280| `anew` | ✗ diverge | ✓ **REPRODUCE** |
281| `gron` | ✗ diverge | ✓ **REPRODUCE** |
282| `age` | ✗ diverge (¡y le faltaba `age-inspect`!) | ✓ **REPRODUCE** |
283
284⇒ **El corpus es determinista hoy.** Lo que hay es deriva contra artefactos viejos.
285
286### Qué significa para la campaña, sin adornos
287- **El argumento de reproducibilidad para el split de debug se cae.** El split se justifica por
288 ESPACIO (~60%, §6), que sigue siendo real y grande. Nada más.
289- **Pero la deriva es un problema por derecho propio, y mayor**: el store contiene artefactos que el
290 lab de hoy **no reproduciría**. Mientras eso siga así, la frase «nuestros artefactos son
291 verificables por terceros» es falsa para una parte del corpus — un tercero que reconstruya no
292 obtendrá lo publicado. `age` es el caso feo: la reconstrucción **ni siquiera instala los mismos
293 ficheros** (falta `age-inspect`), o sea que la deriva no es cosmética.
294- ⇒ La reconstrucción masiva **sigue valiendo la pena**, pero por una razón distinta de la que este
295 documento decía: no para *arreglar* el determinismo, sino para **poner el store al día con el lab**
296 y que la promesa de reproducibilidad sea cierta sobre todo el corpus.
297
298### La lección, que es la misma tres veces
299Este documento se equivocó tres veces seguidas y las tres se corrigieron midiendo:
3001. «el `-ffile-prefix-map` no hace falta» — falso, por leer una sola muestra;
3012. «sí hay no-determinismo» — falso, por un test que confundía deriva con no-determinismo;
3023. y de paso, el «79%» que medía contenido binario y se citaba como tamaño de artefacto.
303
304**Un test hay que diseñarlo contra la pregunta, no contra lo que es fácil de comparar.** Comparar
305con lo que hay en el store es cómodo; comparar dos builds de hoy es lo que contesta.
306
307---
308
309## 8. Etapa 4 en marcha — y la lección que faltaba en el plan: **el orden importa más que la tanda**
310
311La primera tanda activó `strip_debug` en `expat`, `zstd` y `ncurses`. Son **bibliotecas base**, así
312que re-hashearon en cascada **54 recetas**, incluida la cadena `wlroots`/`sway` sellada la noche
313anterior. No se pierde nada —los artefactos viejos siguen en el store y en el respaldo, y las recetas
314están en git— pero el perfil `escritorio-sway` deja de cerrar 121/121 hasta reconstruirlas.
315
316⇒ **La tanda correcta no es «las 25 siguientes por orden alfabético», sino las HOJAS del grafo
317primero, o un nivel entero de una vez con la granja arriba.** Tocar tres bibliotecas base costó 54
318reconstrucciones; tocar cincuenta hojas no habría costado ninguna extra. `scripts/desplegar-strip.sh`
319toma las candidatas en orden alfabético y eso hay que cambiarlo: debe ordenar por profundidad en el
320grafo, de hoja a raíz.
321
322### Operación: dos cosas que cuestan una corrida cada una
323- **Un drenaje largo NO puede vivir dentro del `ssh`.** La primera pasada se cortó a las 14 de 54 al
324 caerse la sesión. Va con `nohup` en el worker, siempre.
325- **La flota efímera reusa IPs** ⇒ la clave de host cambia y cada conexión grita MITM. Se limpia con
326 `ssh-keygen -R <ip>`, pero con workers que van y vienen esto se repetirá: conviene que `farm-up` lo
327 haga solo al crear la máquina.
328
329### Fallos de la primera pasada — ninguno nuevo, y uno que ya estaba avisado
330| receta | causa |
331|---|---|
332| `dwarves` | el muro de libdw/musl, ya documentado como frente propio |
333| `dbus` | «Unhandled python exception» de meson |
334| `adwaita-hello` | **error 39 (Directory not empty)** — la carrera del árbol de fuentes del **ADR 0012** |
335
336El último merece subrayado: **ADR 0012 lleva meses «pendiente sin decidir» y acaba de morder**. Dos
337builds de la misma dep se pisan el árbol de `work/sources` y lo dejan a medias. Una campaña de
338reconstrucción masiva es exactamente el escenario que lo dispara, así que decidirlo deja de ser
339opcional si la etapa 4 va a correr en paralelo.
340
341---
342
343## 9. 🧱 EL BLOQUEANTE ESTRUCTURAL DE LA GRANJA: el worker no tiene Rust ni Go
344
345Medido el 2026-08-08 al lanzar COSMIC:
346
347```
348worker: cargo AUSENTE · rustc AUSENTE · go AUSENTE
349hub: cargo ✓ · rustc ✓ · go ✓ (~/.cargo/bin)
350```
351
352`takana` invoca **`cargo vendor` en el HOST**, no dentro del sandbox hermético — el vendoreo pasa
353antes de entrar a la caja. El worker no trae ningún toolchain de Rust ni Go, así que **la granja no
354puede construir NINGUNA receta Rust o Go**:
355
356- **COSMIC entero** (todos los `cosmic-*` son Rust) — de ahí que 30 raíces fallaran de golpe con
357 `spawn cargo vendor: No such file`;
358- las **228 recetas Rust** y las **362 Go** del corpus.
359
360O sea que **el 76% del catálogo sólo puede construirse en el hub**, que es una sola máquina — justo
361lo que el respaldo del 2026-08-07 intentaba dejar de asumir.
362
363### Lo que esto explica hacia atrás
364Todos los «no construyó» de Go/Rust en el worker durante esta campaña —`amp`, `anew`, `apko`,
365`atlas`, `bom`, `broot`, `cargo-audit`, `cargo-hack`, `git-absorb`— eran **la misma causa**, y yo la
366atribuí primero a «necesitan red para sus módulos». La red no tenía nada que ver: **falta el
367binario**.
368
369### Y no rompe la hermeticidad arreglarlo
370`cargo`/`go` aquí son **herramientas de FETCH**, del mismo orden que `git` para clonar un repo: se
371usan para traer y fijar las deps ANTES del build, no dentro del sandbox. Instalarlos en la imagen
372golden del worker no relaja el aislamiento del build — el build sigue ocurriendo en la caja con el
373árbol ya vendoreado. Es exactamente lo contrario del caso «no engordar el rootfs del worker», que
374habla del rootfs DEL SANDBOX.
375
376⇒ **Es una decisión de aprovisionamiento, no de arquitectura**, y desbloquea tres cuartas partes del
377catálogo para la granja.
378
379## 10. 🔁 La misma herida, reabierta por el renombre (2026-09-14)
380
381El §9 cerró con «es una decisión de aprovisionamiento, no de arquitectura». **Se tomó**: el
3822026-08-28 `bootstrap-devfs.sh` enlazó `go` en `~/.cargo/bin` del worker, que es lo que el fetch
383mira. Funcionó.
384
385Y volvió a romperse **sin que nada fallara**. El enlace guardaba una ruta ABSOLUTA
386—`/opt/hammer/.dev-fs/tools/go/bin/go`— y el renombre hammer→takana (ADR 0016, 2026-09-09) movió el
387directorio. `ln -sfn` había dejado un enlace colgado, y el script imprimía «go enlazado ✓» sin
388comprobar nada. **Del 2026-09-09 al 2026-09-14 el worker no pudo construir ninguna receta Go.**
389
390### Cómo se encontró, que es la parte reutilizable
391
392Por casualidad, verificando reproducibilidad: **16 de 25 recetas Go murieron en SEGUNDOS**. Esa es
393la firma de un fallo SISTÉMICO, nunca la de 16 recetas rotas — es la misma pista que el disco lleno.
394El error lo decía entero:
395
396 Error: spawn go mod vendor: No such file or directory (¿está `go` en el PATH del host?)
397
398pero `verificar-repro.sh` manda la salida del build a `/dev/null`, así que el informe sólo decía
399«no construyó». ⇒ **Ante un «no construyó», reproducir UNA a mano y leer el error** antes de
400escribir un número de deuda.
401
402### Comprobado en los dos sentidos
403
404Repuesto el enlace, `gron` pasó de no construir a construir **y reproducir bit a bit**
405(`why-differs`: 5 entradas idénticas, 0 divergen), y `checkmake` —que la tanda anterior había dado
406por «no construyó»— reproduce. No estaban rotas: faltaba el binario.
407
408`cargo`, `git`, `patch`, `tar` y `curl` del worker se revisaron a la vez y están sanos: `cargo` es
409una instalación de rustup de verdad, no un enlace al repo, y por eso el renombre no lo tocó.
410
411### Lo que se cambió para que no se repita
412
413`bootstrap-devfs.sh` ya no da el enlace por bueno: repone el que esté colgado (avisando de a dónde
414apuntaba) y **comprueba que RESUELVE ejecutando `go version` a través de él**; si no ejecuta, sale 1
415con el síntoma escrito. Probado con un control que tiene que pasar y con dos roturas a propósito
416(enlace colgado a `/opt/hammer` ⇒ lo repone; destino inexistente ⇒ falla ruidoso).
417
418Y de paso, el mismo renombre había dejado **ciegas cuatro sondas de proceso** que buscaban
419`release/hammer`: `estado-granja.sh` informaba «moliendo: (nada — idle)» con el worker compilando
420`centrifugo`, y el `hay_trabajo` del dead-man tampoco veía el build — en una caja hcloud eso es un
421worker borrado a mitad de un build. Las cuatro aceptan ahora los dos nombres.
422
423> **La lección, que es nueva respecto de las tres del §7:** un renombre no rompe sólo lo que
424> compila. Rompe las cadenas que alguien escribió a mano —enlaces absolutos, patrones de `pgrep`,
425> rutas en scripts— y ésas no las mira ningún compilador. Lo que las encuentra es un guardián que
426> comprueba el EFECTO (¿corre `go`?, ¿veo el build?) en vez del gesto (¿creé el enlace?).
427
428### Rendimiento de la campaña ese día
429
430Con el worker devuelto al frente Go y el hub en la familia KDE Frameworks:
431
432| frente | familia | reproducen | deriva | no-determinismo | no construyeron |
433|---|---|---|---|---|---|
434| hub | KF6 tier 1–3 (CMake) | 36 | 0 | 0 | 0 |
435| worker | Go | 17 | 2 | 1 | 0 |
436
437El único no-determinismo real es `gocryptfs`, con los dos ejemplares guardados en
438`store/.divergen/`. **La hipótesis obvia se midió y salió FALSA**: se atribuyó al `cgo = true`
439—porque el temporal `go-build<aleatorio>` queda en el DWARF— pero las otras dos recetas con cgo del
440corpus, `sq` y `usql`, **reproducen**. Y mirado de cerca, la ruta aleatoria tampoco es la causa: los
441dos binarios tienen cadenas IDÉNTICAS salvo esas dos rutas, y sin embargo difieren en **1.874.136
442bytes** empezando dentro de `.rodata`. O sea que hay algo de orden/direcciones debajo, no una cadena
443horneada. Queda ABIERTO y sin causa; `gocryptfs` no está en ningún perfil y no tiene dependientes,
444así que no bloquea ninguna imagen.
23-plan-rehasheo.md
SDD 23 — Plan del re-hasheo masivo (corregido: la etapa 1 desmintió mi primera conclusión)
Escrito 2026-08-08, tras la decisión del usuario de hacer «ahora, antes de seguir creciendo» el
re-hasheo que llevaba tiempo pendiente.
La campaña se había planteado como dos trabajos que exigen reconstruir casi todo el corpus:
-ffile-prefix-map global, para cerrar el «no-determinismo probable de 92 paquetes».
- Separar la información de depuración en paquetes
-debug.
Los dos son reales. La primera versión de este documento decía que el primero se cancelaba
«porque su premisa es falsa» — me equivoqué, y la etapa 1 lo demostró en veinte minutos. La
corrección está en el §1.bis, y es el mejor argumento a favor de tener puertas.
1. Lo primero fue verificar la premisa (y me pasé de confiado — ver §1.bis)
El plan heredado decía: «92 paquetes con no-determinismo probable, todos por rutas /src en
.debug_*; pendiente: -ffile-prefix-map global re-hashea ~720». Antes de comprometer días de
granja, se comprobó. Tres medidas, en orden de fuerza creciente:
(a) Las rutas /src son CONSTANTES, no variables. El lab bindea el árbol de fuentes en /src
—literal, siempre— así que una ruta /src/foo.c embebida en .debug_ es idéntica en cada rebuild y
en cualquier máquina. No es no-determinismo: es una ruta fija que parece sospechosa.
(b) Barrido de 195 artefactos buscando rutas que sí variarían (/home/<user>/,
/opt/hammer/work, /tmp/<aleatorio>): 15 las tienen (≈8%). Pero al mirarlas una a una,
ninguna es nuestra:
| ruta hallada | qué es realmente |
/home/buildozer/aports/main/musl/src/musl-1.2.6 | la ruta de compilación de Alpine, horneada en el musl prebuilt que inyectamos |
/tmp/apiresponse.json | una cadena literal en el código fuente de gron |
/home/jimmac/, /home/sam/dev/ | rutas de los autores dentro de los assets SVG de adwaita |
Las tres son constantes que vienen de fuera y son idénticas en cada rebuild. Buscar rutas con
pinta de host no mide no-determinismo: mide qué escribieron terceros en sus fuentes. Es la misma
clase de error que la regla del .a no-PIC —grep R_X86_64_32 contaba reubicaciones de
.debug_*— y que el conteo de licencias con grep -l license, que contaba comentarios.
(c) La prueba que decide: reconstruir y comparar. Se apartó el artefacto de wl-clipboard, se
reconstruyó con las deps cacheadas y se comparó con takana why-differs:
24 entradas idénticas · 0 divergen
✓ los dos árboles son idénticos: REPRODUCE
⇒ Conclusión que saqué entonces: «la reproducibilidad se sostiene, el -ffile-prefix-map no hace
falta». Era falsa, y duró lo que tardó en correrse la etapa 1. Ver §1.bis.
La lección de método, que vale más que el ahorro: el no-determinismo se mide reconstruyendo y
comparando, no buscando cadenas sospechosas. Un grep sobre binarios genera candidatos, no
veredictos. Y este proyecto ya tiene la herramienta del veredicto (why-differs); lo que faltaba
era usarla antes de planificar.
⚠️ Esto no prueba que las 1153 reproduzcan — sólo que UNA lo hace. Yo lo leí como si probara
más de lo que probaba, que es exactamente el error contra el que este mismo documento advierte
dos párrafos más arriba.
1.bis 🔴 CORRECCIÓN: sí hay no-determinismo, y el -ffile-prefix-map SÍ hace falta
La etapa 1 (verificación amplia, scripts/verificar-repro.sh) se corrió sobre una muestra por clase
de build. Resultado sobre las que pudieron reconstruirse:
| receta | veredicto |
binutils | ✓ REPRODUCE |
appstream | ✗ DIVERGE |
bison | ✗ DIVERGE |
wl-clipboard (§1c) | ✓ REPRODUCE |
Y la causa, idéntica en las dos que divergen, dicha por why-differs:
difieren [.debug_aranges, .debug_info, .debug_pubnames, .debug_pubtypes, .debug_str]
— sólo info de depuración (el código ejecutable es idéntico)
· .debug_str sólo en B: /src/output/meson-private
Dónde me equivoqué, exactamente: el barrido de §1b buscaba rutas del host (/home/<user>/,
/tmp/<aleatorio>) y no encontró ninguna nuestra — cierto. Pero el no-determinismo no venía de una
ruta del host sino de rutas internas al árbol de build (/src/output/meson-private) que varían
entre corridas aunque /src sea constante. Buscar la forma equivocada de ruta y no hallarla no
prueba que no haya otra. Y una sola muestra que reproduce (§1c) no es una muestra.
⇒ La lección del §1 sigue siendo válida y encima se refuerza: el veredicto es reconstruir y
comparar. Sólo que esta vez el equivocado fui yo, y lo que me salvó fue haber puesto la puerta
antes de la campaña en vez de después.
🎁 Y de ahí sale la mejor noticia del plan
Las dos mitades son el mismo trabajo. La divergencia vive ENTERA en secciones .debug_* y el
código ejecutable es idéntico ⇒ separar el debug del artefacto principal hace que el artefacto
principal reproduzca, sin necesidad de tocar -ffile-prefix-map en 720 recetas.
Queda por decidir qué se hace con el paquete -debug en sí (su contenido seguiría siendo
no-determinista sin -ffile-prefix-map), pero eso es un problema mucho más chico: afecta a un
artefacto secundario que nadie instala por defecto, y puede resolverse después o a la vez.
2. Lo que sí queda: separar la información de depuración
Medido sobre muestra amplia de .so/.a: el 79% del contenido binario del store son secciones
.debug_* (qtdeclarative solo llega al 87%). Consecuencias, las dos que importan:
- ~96 G de reserva de disco — el store de ~126 G serían ~30 G sin debug.
- El espejo público baja de 126 G a ~30 G, que es la diferencia entre alojar barato y caro, y
toca la decisión ya tomada de servirlo desde el Storage Box.
Y no es sólo tamaño: separar el debug es lo que permite entregar los símbolos a quien depura sin
imponérselos a quien sólo usa la distro, que es lo que hacen todas las distros serias.
El precio, dicho claro
strip posterior no sirve: rompería la verificación bit-a-bit, porque el ArtifactHash es de
ENTRADA (se calcula sin construir) y quedaría apuntando a un contenido que un rebuild ya no
reproduce. Hay que hacerlo dentro del build, y eso cambia la fase install ⇒ re-hashea las
recetas afectadas. Es el coste real y no hay atajo.
⚠ Pero para la IMAGEN sí hay atajo, y es legítimo (2026-09-23)
Lo de arriba vale para el STORE. Un rootfs proyectado no es un artefacto: se arma copiando y
enlazando, no se sella, y nadie verifica su hash contra una receta. Stripear ahí no rompe nada —
strip es determinista, así que la imagen sigue siendo reproducible— y no toca ni un byte del
store, que conserva los símbolos para quien depura. Es exactamente lo que el propio §2 pide:
«entregar los símbolos a quien depura sin imponérselos a quien sólo usa la distro».
Medido sobre el rootfs de la variante mirada del instalador:
| |
| tal como se proyecta hoy | 1348 MiB |
con strip --strip-unneeded sobre los ejecutables | 891 MiB — un 34 % menos |
un binario suelto, openssh: ssh | 19,3 → 5,3 MiB, un 72 % |
La distancia entre el 34 % y el 72 % es la composición: en un escritorio buena parte de los bytes
son datos —fuentes, iconos, traducciones— que no tienen símbolos. En servidor, dominado por
binarios estáticos grandes (rust 364 MiB, gitea 219, git 216, openssh 202), el ahorro sería
mayor. No está medido para servidor y por eso no se pone un número.
⇒ Es la palanca barata para el tamaño de las descargas, y no sustituye al split dentro del
build: eso sigue siendo lo que arregla los 126 G del store y del espejo.
3. 🧨 El hallazgo que condiciona CÓMO se hace: el entorno del lab NO está hasheado
Al buscar dónde poner una flag global apareció algo que hay que decidir antes de tocar nada.
crates/hammer-build/src/sandbox.rs fija el entorno de TODOS los builds:
CC=takana-zig-cc (con -mcpu=baseline) SOURCE_DATE_EPOCH=1 TZ=UTC LC_ALL=C
AR="zig ar" CARGO_BUILD_… codegen-units=1
Ninguna de esas entra en Recipe::hash_inputs, que es una lista blanca de source, compiler,
target, link, zig_version, patches, flags, phases y deps.
⇒ Cambiar el entorno del lab cambia lo que sale del build SIN cambiar un solo hash. El store
diría que todo está al día mientras los artefactos ya no corresponden a su hash. Es el fallo más
peligroso posible en un sistema direccionado por contenido: no falla, miente.
Hoy no muerde porque ese entorno no cambia nunca. Pero una campaña global es precisamente el momento
en que cambiaría.
Las dos formas de hacerlo, y cuál propongo
- A — flag en el lab (una línea en
sandbox.rs, aplica a todo): barata de escribir y
rompe el invariante por lo de arriba. Descartada, salvo que se añada primero un
lab_version a hash_inputs (ver abajo).
- B — en la fase
install de cada receta (strip --only-keep-debug + objcopy): explícita,
entra al hash por diseño, re-hashea sólo lo que toca, y es auditable receta a receta.
Propuesta. El coste es editar ~1100 recetas, pero es mecánico y verificable — exactamente el
mismo patrón con el que hoy se poblaron 1074 licencias sin mover un hash.
Y un ticket previo, chico y de fondo: añadir a hash_inputs un lab_version que capture el
entorno del sandbox, con el mismo diseño que zig_version (sólo entra si está fijado, así los
hashes actuales no se mueven). Sin eso, cualquier cambio futuro del lab vuelve a ser invisible.
Es barato hoy y caro el día que haga falta.
4. El plan, por etapas y con puertas
Cada etapa tiene una puerta: si no pasa, se para. Una campaña de días sin puertas es una forma
elegante de romper el corpus.
| # | etapa | puerta |
| 0 | lab_version en hash_inputs (opcional, no fijado) | los 1153 hashes actuales no se mueven |
| 1 | Verificación AMPLIA de reproducibilidad: apartar y reconstruir una muestra de ~30 recetas de clases distintas (C, Rust, Go, meson, cmake) y comparar con why-differs | 0 divergencias; si alguna diverge, ESO es el trabajo real y esta campaña se pospone |
| 2 | Piloto del split de debug en 5 recetas representativas | los -debug salen, el binario sigue funcionando, y el rebuild reproduce bit a bit |
| 3 | Medir de verdad el ahorro y extrapolar — ✅ HECHO, ver §6 | el ahorro real es ~60%, no 79%: se replanteó |
| 4 | Aplicar al corpus por tandas, con la granja | cada tanda: store-gc + why-differs sobre una muestra |
| 5 | Regenerar grafos, perfiles y el espejo | los perfiles siguen cerrando (escritorio-sway 121/121, KDE 162/162…) |
Orden respecto de lo demás: la etapa 1 conviene ANTES que nada, porque si la reproducibilidad
amplia no se sostiene, todo lo demás cambia de prioridad. Y es barata: apartar artefactos y
reconstruir con deps cacheadas, como se hizo con wl-clipboard.
5. Lo que este plan NO propone
- No tocar
-ffile-prefix-map: §1 lo cerró.
- No strippear artefactos ya sellados: rompe la evidencia, que es el invariante del proyecto.
- No meter la flag en el lab sin
lab_version: §3.
- No empezar por el corpus entero: §4 empieza por 5 recetas, y con razón.
6. Etapa 3 — el ahorro real es ~60%, no 79%, y las cifras publicadas estaban infladas
El «79%» que este documento y los SDD 19/20 venían citando era el 79% del CONTENIDO BINARIO,
medido sobre una muestra de .so/.a. Es cierto y es la cifra equivocada para planificar, porque
un artefacto no es sólo binarios: trae cabeceras, datos, iconos, locales, .pc y documentación,
que no encogen.
Medido con scripts/medir-debug.sh (suma los tamaños reales de las secciones .debug_* vía
readelf -S y los compara con el tamaño del árbol, sin reconstruir nada):
| muestra | artefactos | volumen | debug / artefacto |
| pequeña | 40 | 876 MB | 27% |
| media (truncada a 60 ficheros/artefacto) | 100 | 2 955 MB | 38% |
| grande, sin truncar | 250 | 8 874 MB | 60% |
La varianza es alta porque unos pocos artefactos grandes dominan el total. La muestra grande es la
que manda (cubre ~7% del store), y encaja con lo único que está medido de verdad —el piloto de la
etapa 2, donde se reconstruyó y se pesó:
bison 6 M → 3 M = −50%
appstream 56 M → 18 M = −68%
Los dos encierran el 60% de la muestra grande. Ésa es la validación: el modelo predice lo que el
rebuild real produjo.
Las cifras corregidas
Con el store en 127 G y un ahorro de ~60%:
| se venía diciendo | medido |
| reserva de disco | ~96 G | ~76 G |
| store tras el split | ~30 G | ~51 G |
| espejo público | ~30 G | ~51 G |
Sigue siendo el mayor ahorro disponible en el proyecto —76 G no es poco— pero no es lo prometido,
y la diferencia importa para dimensionar el alojamiento del espejo, que es una decisión con factura.
La lección: una cifra medida sobre una cosa (contenido binario) se citó durante semanas como si
midiera otra (tamaño de artefacto), y llegó a tres documentos. El número no era falso; la unidad
sí. Cuando un número vaya a decidir un gasto, conviene volver a la medición original y comprobar
QUÉ estaba midiendo.
7. 🔴 SEGUNDA CORRECCIÓN: lo que medía el test era DERIVA, no no-determinismo
El §1.bis dijo «sí hay no-determinismo» al ver que appstream y bison divergían. También estaba
mal, y por un fallo de diseño del propio test.
scripts/verificar-repro.sh aparta el artefacto guardado en el store y lo compara con una
reconstrucción de hoy. Pero ese artefacto guardado puede tener meses: se construyó con otro estado
del lab (otro zig, otras flags, otro entorno). Así que el test conflaba dos cosas muy distintas:
- no-determinismo — mismas entradas, mismo lab, salidas distintas. Es lo que rompe el invariante.
- deriva — el artefacto viejo no es lo que el lab de HOY produce. No es no-determinismo: es que
el mundo se movió.
La prueba que las separa: construir DOS VECES HOY. Y se hizo, sin querer al principio: al volver
a correr el verificador sobre recetas ya reconstruidas, todas pasaron a REPRODUCIR.
| receta | 1ª corrida (viejo vs hoy) | 2ª corrida (hoy vs hoy) |
anew | ✗ diverge | ✓ REPRODUCE |
gron | ✗ diverge | ✓ REPRODUCE |
age | ✗ diverge (¡y le faltaba age-inspect!) | ✓ REPRODUCE |
⇒ El corpus es determinista hoy. Lo que hay es deriva contra artefactos viejos.
Qué significa para la campaña, sin adornos
- El argumento de reproducibilidad para el split de debug se cae. El split se justifica por
ESPACIO (~60%, §6), que sigue siendo real y grande. Nada más.
- Pero la deriva es un problema por derecho propio, y mayor: el store contiene artefactos que el
lab de hoy no reproduciría. Mientras eso siga así, la frase «nuestros artefactos son
verificables por terceros» es falsa para una parte del corpus — un tercero que reconstruya no
obtendrá lo publicado.
age es el caso feo: la reconstrucción ni siquiera instala los mismos
ficheros (falta age-inspect), o sea que la deriva no es cosmética.
- ⇒ La reconstrucción masiva sigue valiendo la pena, pero por una razón distinta de la que este
documento decía: no para arreglar el determinismo, sino para poner el store al día con el lab
y que la promesa de reproducibilidad sea cierta sobre todo el corpus.
La lección, que es la misma tres veces
Este documento se equivocó tres veces seguidas y las tres se corrigieron midiendo:
- «el
-ffile-prefix-map no hace falta» — falso, por leer una sola muestra;
- «sí hay no-determinismo» — falso, por un test que confundía deriva con no-determinismo;
- y de paso, el «79%» que medía contenido binario y se citaba como tamaño de artefacto.
Un test hay que diseñarlo contra la pregunta, no contra lo que es fácil de comparar. Comparar
con lo que hay en el store es cómodo; comparar dos builds de hoy es lo que contesta.
8. Etapa 4 en marcha — y la lección que faltaba en el plan: el orden importa más que la tanda
La primera tanda activó strip_debug en expat, zstd y ncurses. Son bibliotecas base, así
que re-hashearon en cascada 54 recetas, incluida la cadena wlroots/sway sellada la noche
anterior. No se pierde nada —los artefactos viejos siguen en el store y en el respaldo, y las recetas
están en git— pero el perfil escritorio-sway deja de cerrar 121/121 hasta reconstruirlas.
⇒ La tanda correcta no es «las 25 siguientes por orden alfabético», sino las HOJAS del grafo
primero, o un nivel entero de una vez con la granja arriba. Tocar tres bibliotecas base costó 54
reconstrucciones; tocar cincuenta hojas no habría costado ninguna extra. scripts/desplegar-strip.sh
toma las candidatas en orden alfabético y eso hay que cambiarlo: debe ordenar por profundidad en el
grafo, de hoja a raíz.
Operación: dos cosas que cuestan una corrida cada una
- Un drenaje largo NO puede vivir dentro del
ssh. La primera pasada se cortó a las 14 de 54 al
caerse la sesión. Va con nohup en el worker, siempre.
- La flota efímera reusa IPs ⇒ la clave de host cambia y cada conexión grita MITM. Se limpia con
ssh-keygen -R <ip>, pero con workers que van y vienen esto se repetirá: conviene que farm-up lo
haga solo al crear la máquina.
Fallos de la primera pasada — ninguno nuevo, y uno que ya estaba avisado
| receta | causa |
dwarves | el muro de libdw/musl, ya documentado como frente propio |
dbus | «Unhandled python exception» de meson |
adwaita-hello | error 39 (Directory not empty) — la carrera del árbol de fuentes del ADR 0012 |
El último merece subrayado: ADR 0012 lleva meses «pendiente sin decidir» y acaba de morder. Dos
builds de la misma dep se pisan el árbol de work/sources y lo dejan a medias. Una campaña de
reconstrucción masiva es exactamente el escenario que lo dispara, así que decidirlo deja de ser
opcional si la etapa 4 va a correr en paralelo.
9. 🧱 EL BLOQUEANTE ESTRUCTURAL DE LA GRANJA: el worker no tiene Rust ni Go
Medido el 2026-08-08 al lanzar COSMIC:
worker: cargo AUSENTE · rustc AUSENTE · go AUSENTE
hub: cargo ✓ · rustc ✓ · go ✓ (~/.cargo/bin)
takana invoca cargo vendor en el HOST, no dentro del sandbox hermético — el vendoreo pasa
antes de entrar a la caja. El worker no trae ningún toolchain de Rust ni Go, así que la granja no
puede construir NINGUNA receta Rust o Go:
- COSMIC entero (todos los
cosmic-* son Rust) — de ahí que 30 raíces fallaran de golpe con
spawn cargo vendor: No such file;
- las 228 recetas Rust y las 362 Go del corpus.
O sea que el 76% del catálogo sólo puede construirse en el hub, que es una sola máquina — justo
lo que el respaldo del 2026-08-07 intentaba dejar de asumir.
Lo que esto explica hacia atrás
Todos los «no construyó» de Go/Rust en el worker durante esta campaña —amp, anew, apko,
atlas, bom, broot, cargo-audit, cargo-hack, git-absorb— eran la misma causa, y yo la
atribuí primero a «necesitan red para sus módulos». La red no tenía nada que ver: falta el
binario.
Y no rompe la hermeticidad arreglarlo
cargo/go aquí son herramientas de FETCH, del mismo orden que git para clonar un repo: se
usan para traer y fijar las deps ANTES del build, no dentro del sandbox. Instalarlos en la imagen
golden del worker no relaja el aislamiento del build — el build sigue ocurriendo en la caja con el
árbol ya vendoreado. Es exactamente lo contrario del caso «no engordar el rootfs del worker», que
habla del rootfs DEL SANDBOX.
⇒ Es una decisión de aprovisionamiento, no de arquitectura, y desbloquea tres cuartas partes del
catálogo para la granja.
10. 🔁 La misma herida, reabierta por el renombre (2026-09-14)
El §9 cerró con «es una decisión de aprovisionamiento, no de arquitectura». Se tomó: el
2026-08-28 bootstrap-devfs.sh enlazó go en ~/.cargo/bin del worker, que es lo que el fetch
mira. Funcionó.
Y volvió a romperse sin que nada fallara. El enlace guardaba una ruta ABSOLUTA
—/opt/hammer/.dev-fs/tools/go/bin/go— y el renombre hammer→takana (ADR 0016, 2026-09-09) movió el
directorio. ln -sfn había dejado un enlace colgado, y el script imprimía «go enlazado ✓» sin
comprobar nada. Del 2026-09-09 al 2026-09-14 el worker no pudo construir ninguna receta Go.
Cómo se encontró, que es la parte reutilizable
Por casualidad, verificando reproducibilidad: 16 de 25 recetas Go murieron en SEGUNDOS. Esa es
la firma de un fallo SISTÉMICO, nunca la de 16 recetas rotas — es la misma pista que el disco lleno.
El error lo decía entero:
Error: spawn go mod vendor: No such file or directory (¿está `go` en el PATH del host?)
pero verificar-repro.sh manda la salida del build a /dev/null, así que el informe sólo decía
«no construyó». ⇒ Ante un «no construyó», reproducir UNA a mano y leer el error antes de
escribir un número de deuda.
Comprobado en los dos sentidos
Repuesto el enlace, gron pasó de no construir a construir y reproducir bit a bit
(why-differs: 5 entradas idénticas, 0 divergen), y checkmake —que la tanda anterior había dado
por «no construyó»— reproduce. No estaban rotas: faltaba el binario.
cargo, git, patch, tar y curl del worker se revisaron a la vez y están sanos: cargo es
una instalación de rustup de verdad, no un enlace al repo, y por eso el renombre no lo tocó.
Lo que se cambió para que no se repita
bootstrap-devfs.sh ya no da el enlace por bueno: repone el que esté colgado (avisando de a dónde
apuntaba) y comprueba que RESUELVE ejecutando go version a través de él; si no ejecuta, sale 1
con el síntoma escrito. Probado con un control que tiene que pasar y con dos roturas a propósito
(enlace colgado a /opt/hammer ⇒ lo repone; destino inexistente ⇒ falla ruidoso).
Y de paso, el mismo renombre había dejado ciegas cuatro sondas de proceso que buscaban
release/hammer: estado-granja.sh informaba «moliendo: (nada — idle)» con el worker compilando
centrifugo, y el hay_trabajo del dead-man tampoco veía el build — en una caja hcloud eso es un
worker borrado a mitad de un build. Las cuatro aceptan ahora los dos nombres.
La lección, que es nueva respecto de las tres del §7: un renombre no rompe sólo lo que
compila. Rompe las cadenas que alguien escribió a mano —enlaces absolutos, patrones de pgrep,
rutas en scripts— y ésas no las mira ningún compilador. Lo que las encuentra es un guardián que
comprueba el EFECTO (¿corre go?, ¿veo el build?) en vez del gesto (¿creé el enlace?).
Rendimiento de la campaña ese día
Con el worker devuelto al frente Go y el hub en la familia KDE Frameworks:
| frente | familia | reproducen | deriva | no-determinismo | no construyeron |
| hub | KF6 tier 1–3 (CMake) | 36 | 0 | 0 | 0 |
| worker | Go | 17 | 2 | 1 | 0 |
El único no-determinismo real es gocryptfs, con los dos ejemplares guardados en
store/.divergen/. La hipótesis obvia se midió y salió FALSA: se atribuyó al cgo = true
—porque el temporal go-build<aleatorio> queda en el DWARF— pero las otras dos recetas con cgo del
corpus, sq y usql, reproducen. Y mirado de cerca, la ruta aleatoria tampoco es la causa: los
dos binarios tienen cadenas IDÉNTICAS salvo esas dos rutas, y sin embargo difieren en 1.874.136
bytes empezando dentro de .rodata. O sea que hay algo de orden/direcciones debajo, no una cadena
horneada. Queda ABIERTO y sin causa; gocryptfs no está en ningún perfil y no tiene dependientes,
así que no bloquea ninguna imagen.