4190c7b43e5bd323a9b166340cfa16cd324ca2e4
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
476168bb07 |
takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa. El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces: las tres eran el MOTD que el script escribe DENTRO de la imagen construida — texto del producto, no comentario del script. Se cambiaron aparte y a propósito, que es rebranding, no limpieza. Y el hallazgo caro: casaba contra , que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate. La etapa 4 lo movió a y el script quedó casando NADA. No fallaba: imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja. Además 14 rutas de módulo en docs, que el barrido anterior no tocó porque no es frontera de palabra. |
||
|
|
1c3e185167 |
takana: las variables de entorno leen las dos formas, gana la nueva
TAKANA_X con caída a HAMMER_X (ADR 0016). No es un sed: estas variables son
contrato de usuario —knobs del instalador, el entorno del worker, mirror-env.sh—
y viven en perfiles de shell y units FUERA del repo, así que un renombre duro no
falla ruidosamente: la variable no aparece, se toma el default y el build se
comporta distinto sin que nada lo diga.
Rust: takana_core::env::{var,var_os} toma el nombre canónico y deriva el viejo
cambiando el prefijo — se le pasa TAKANA_* para que un grep del nombre nuevo
encuentre todas las lecturas. 5 tests, con DOS controles negativos: sin ninguna
de las dos no hay valor, y un nombre sin prefijo no inventa una caída.
Migrados los 17 sitios directos y los indirectos que el grep no mostraba
(bases_de_mirror, las constantes de kernel_cmd, ROOT_ENV de qorpa, env_path de
recover). takana-recover lleva la caída inline: es un mini-binario que se copia
a /usr/sbin y no vale arrastrarle una dep entera por dos líneas.
Scripts: 30 lecturas pasan a default, manteniendo el
nombre INTERNO de la variable para no tocar sus 190 usos.
Y donde el script EXPORTA en vez de leer, se ponen LAS DOS (mirror-env.sh y el
fragmento in-VM de bootstrap): ahí el lector puede ser un binario viejo —worker
sin recompilar, el /usr/bin/hammer pinado del baseline— que sólo conoce HAMMER_*.
La caída sirve para lectores nuevos; los viejos necesitan que la vieja siga puesta.
Verificado de punta a punta con el binario, no sólo con unit tests: HAMMER_LAB
sigue surtiendo efecto, TAKANA_LAB hace lo mismo, y con las dos gana TAKANA_LAB.
605 tests en verde y el hash de zlib sigue en b3:dc363f26… , intacto.
Cambio de comportamiento que va aparte y hay que decir: el hostname por defecto
de una instalación nueva pasa de 'hammer' a 'takana' (sólo si no se fija ninguna
de las dos variables).
NO se tocan: las rutas /var/lib/hammer de sistemas instalados, el volid
HAMMER_LIVE del ISO, ni el namespace HARKAQ_*, que es de otro subsistema.
|
||
|
|
8730aad34e |
takana etapa 3b: las 49 invocaciones pasan a ./target/release/takana
Los llamadores EJECUTABLES: scripts/ (incluida toda la granja), los runbooks y CLAUDE.md. Seguro porque la 3a ya garantiza que el worker emite los dos binarios, y porque en farm-lab-sync.sh el cargo build remoto precede a la invocación remota en el mismo script. Verificado: sintaxis de los 49 (bash -n / py_compile — ojo que why-differs-barrido.sh es Python con extensión .sh) y `takana hash` devuelve hash real sobre el store. NO se toca en esta etapa, a propósito: - La variable de entorno HAMMER=. Es interfaz de los scripts entre sí y hay llamadores que la fijan; renombrarla va con la etapa 4. - docs/evidencia/ y los HANDOFF: son REGISTRO de lo que se corrió ese día. Reescribir un comando dentro de una evidencia la falsifica. - docs/state/: es generado, se regenera solo. - Los ADR y los docs de diseño: texto, y `hammer` sigue funcionando. Van con la etapa 5, que es la de churn de texto. |
||
|
|
8a2c6fad39 |
repro: «no pude comparar» tampoco es no-determinismo — segundo sitio, misma lección
Una tanda volvió a llenar el libro de veredictos falsos: 47 recetas sanas anotadas como
NO-DETERMINISMO. Esta vez la cadena empezó antes de donde yo había mirado.
El `mv` que APARTA el artefacto falló (el directorio de apartado no estaba) y yo nunca comprobaba que
hubiera funcionado. A partir de ahí todo lo que sigue es basura: el artefacto se queda en su sitio,
`hammer why-differs` compara contra una ruta que no existe y devuelve `Error: store: no existe…`, y
mi código leía cualquier salida no-cero como «difieren». Un error de comparación disfrazado de
veredicto, 47 veces.
Es la MISMA lección que arreglé hace un rato para el build, en un segundo sitio que no miré:
**no poder medir no es un resultado negativo**. Dos guardas nuevas:
· el apartado se comprueba (`mv || SIN VEREDICTO`) y la receta se salta ruidosamente;
· `why-differs` se lee, no sólo su exit code: si dice `Error:`/`no existe`, es SIN VEREDICTO, se
restaura el artefacto y NO se anota nada.
Probadas las dos: con el apartadero sin permiso de escritura sale «no pude apartar el artefacto ⇒
SIN VEREDICTO», el artefacto queda intacto y el libro no crece.
Comprobado además que la tanda mala no perdió NADA: como el `mv` fallaba, los 47 artefactos nunca
salieron del store (`faltan: 0`). El daño fue sólo el registro, y está purgado — el libro queda con
51 verificaciones buenas.
La regla, ya por triplicado hoy: cuando un guardián no puede establecer algo, el estado es *sin
veredicto*, y eso no se escribe. Un libro que anota lo que no midió se cree igual que uno que sí.
|
||
|
|
a48885e143 |
repro: podar el árbol de fuentes tras cada veredicto sano — el barrido ya no llena el disco
Un barrido dejaba un árbol extraído por receta en `work/sources` y no limpiaba hasta el final. En el
hub eso llevó el disco del 95% al 98% DOS VECES hoy, y un disco lleno no se lee como disco lleno: se
lee como recetas rotas. Con la poda por veredicto, `work/sources` se queda en 3 MB durante toda la
tanda en vez de crecer hasta 7,7 G.
No cuesta nada porque `fetch.rs` borra y re-extrae el árbol en CADA build por diseño: guardarlo entre
recetas de un barrido no ahorra un segundo.
Dos límites deliberados:
· Sólo se poda cuando el veredicto es SANO. Si algo falló o divergió, el árbol es justamente lo que
hace falta para mirarlo — un verificador que limpia la escena del problema que acaba de encontrar
sirve para poco. Es la misma razón por la que los ejemplares divergentes se conservan.
· Sólo el árbol de ESA receta. Las deps extraídas se quedan: son compartidas, y borrarlas mientras
otra cosa las usa es exactamente el ADR 0012.
Con esto la cobertura se puede levantar en el HUB, que es donde están los 1157 artefactos. El worker
sólo tiene los que construyó —el último barrido allá reportó 47 de 60 «sin artefacto»— así que como
máquina de cobertura no sirve por más disco que tenga.
|
||
|
|
d7d9cf8fd2 |
repro: «la 2ª reconstrucción no construyó» NO es no-determinismo — mi propio bug, y lo pagó el libro
Corriendo una tanda con `MAX_SECONDS=45` se llenó el disco a mitad, los rebuilds empezaron a morir por ENOSPC, y la rama que yo mismo había escrito para distinguir DERIVA de NO-DETERMINISMO metió «falló» y «difiere» en la misma condición ⇒ **21 recetas sanas quedaron anotadas como NO-DETERMINISMO** en un libro que pretende ser autoritativo. Un registro falso y persistente es peor que no tener registro: el propósito del libro es que nadie tenga que volver a mirar, así que una entrada equivocada se cree para siempre. Es la trampa de siempre —un disco lleno se lee como receta rota, y la pista es que mueren varias seguidas— y el script ORIGINAL ya la evitaba: «no es lo mismo «no reproduce» que «no construye acá». Distinguirlos importa: mezclarlos inventaría un problema de determinismo». Lo rompí al añadir la segunda reconstrucción. Restituido: si la 2ª no construye, se restaura el artefacto, se cuenta como fallo y **no se anota nada** — sin veredicto es un estado legítimo. Reparado además el daño: las 21 entradas falsas purgadas del libro (quedan 27 verificaciones buenas), y `angle-grinder` repuesto desde `store/.divergen/`, donde lo había dejado la clasificación errónea. Eso sí funcionó como debía: el ejemplar estaba guardado y entero, no perdido. Y queda anotado en el propio script el límite del filtro por tiempo que provocó todo: el sidecar de `store/.times/` lo escribe el WORKER, así que acota el build de esa receta EN LA MÁQUINA QUE LO MIDIÓ. Una receta Rust de 40 s allá con la caché de cargo caliente son muchos minutos acá en frío —`angle-grinder` y `amp` entraron por el filtro y se comieron el disco— y encima el número no cuenta la CASCADA de deps que falten en el store local, que es la misma sorpresa que dio `gjs`. Con disco justo conviene pasar las recetas a mano. |
||
|
|
efe6e001d2 |
repro: un libro de lo verificado, y por fin un número de cobertura — era 2 de 1157
`verificar-repro.sh` sorteaba una muestra y se olvidaba. Sin registro no había forma de contestar la
pregunta que importa —¿qué fracción del corpus se comprobó alguna vez que reproduce?— y encima las
mismas recetas salían sorteadas una y otra vez mientras otras no se miraban jamás. La primera medida
con el libro puesto: **2 de 1157**. El invariante central del proyecto no se había verificado de
forma acumulativa prácticamente en nada, y eso no se veía porque cada corrida daba «✅ PUERTA
SUPERADA» sobre su propia muestra.
`docs/state/repro-verificado.tsv` va con la misma clave auto-invalidante que el libro de fuzz:
(receta, **ArtifactHash**). El hash resume la receta MÁS su cierre de deps, así que una entrada deja
de casar en cuanto algo aguas arriba cambia. Un «ya lo verifiqué» que sobreviviera a un re-hash sería
una mentira; éste no puede serlo.
Con eso el muestreo pasa a sortear entre lo NO verificado, así que corridas sucesivas ACUMULAN
cobertura en vez de repetir (con `TODO=1` se sortea entre todas, para re-verificar a propósito), y
`--coverage` da el número sin construir nada.
Verificadas en esta tanda: itstool, markupsafe, musl-obstack, hicolor-icon-theme, musl-fts, npth,
poppler-render-check, doas, libffi y packaging REPRODUCEN; `when` derivaba y quedó al día. Cero
no-determinismos. Van 11 de 1157.
⚠ Y una trampa que casi me come, anotada acá porque el próximo la va a pisar: elegí candidatos «por
artefacto chico» y la lista empezó con **nodejs**, cuyo artefacto pesa 0,2 MB y cuyo build son horas
de V8. El tamaño del artefacto NO dice nada del costo de construirlo. Para elegir muestra barata hay
que mirar tiempos de build, no bytes de salida.
|
||
|
|
d5863ae554 |
verificar-repro: el apartadero iba a tmpfs, no tomaba el lock, y dejaba el veredicto a medias
Tres defectos, encontrados corriéndolo y pagando uno de ellos: perdí el artefacto de `aichat` al interrumpir una corrida. Está reconstruyéndose en el worker. **1. El apartadero estaba en `/tmp`.** Para verificar hay que sacar el artefacto de su sitio y dejar que hammer lo reconstruya; eso iba a un `mktemp -d`. La cabecera prometía «un verificador que destruye lo que verifica es peor que no tenerlo», y esa ubicación rompía la promesa en tres sitios a la vez: `/tmp` es **tmpfs**, así que apartar copiaba el artefacto a RAM en una caja de 7,6 GiB sin swap; al ser otro sistema de ficheros el `mv` no era un rename atómico sino copiar-y-borrar, o sea que una interrupción a mitad lo dejaba en ninguna parte; y lo que sobreviviera moría al reiniciar. Ahora va a `store/.verificar-repro`: el `mv` es instantáneo, no cuesta RAM ni disco, y lo apartado SOBREVIVE a un kill — la corrida siguiente lo repone sola antes de empezar. **2. Llamaba a `hammer build` sin tomar el `flock`.** Es la regla 1 del repo, y saltársela arriesga el árbol compartido de `work/sources` de forma irreversible (ADR 0012). Lo toma el script y no quien lo llama, por lo mismo que `poda-fuentes.sh`: así no hay forma de correrlo mal — y correrlo mal fue exactamente lo que hice. Con `-E 3`, porque sin eso «no conseguí el lock» sale como exit 1, que es lo que este script usa para «hay divergencias»: informar «el corpus no reproduce» cuando lo que pasaba era que había otro build corriendo es peor que no correr. **3. Confundía DERIVA con NO-DETERMINISMO y lo delegaba en la memoria de quien leía.** Que el guardado difiera de una reconstrucción de hoy puede ser que el lab se movió (deriva, sano) o que las mismas entradas dan salidas distintas (no-determinismo, bug). La cabecera decía «corré el script dos veces y mirá la segunda» — con lo que la primera corrida informaba «DIVERGE» sobre cosas sanas y entrenaba a no creerle. Ahora, ante una diferencia, reconstruye una segunda vez y compara las dos reconstrucciones ENTRE SÍ: si coinciden es DERIVA, si no, NO-DETERMINISMO. El rebuild extra sólo se paga cuando ya hubo una diferencia. Y cuando es no-determinismo de verdad, los dos ejemplares se conservan en `store/.divergen/` en vez de borrarse: antes el guardián tiraba la única evidencia justo en el caso que existe para encontrar. Medido de paso, y es la parte buena: `libassuan`, `libksba`, `libffi` y `packaging` —artefactos del 21 de agosto— reproducen BIT A BIT con el lab de hoy. `scdoc` derivaba y ya está al día; `gron`, `age` y `anew` reproducen. La rama de DERIVA se probó ensuciando a propósito una copia guardada: clasifica bien y el store queda con la reconstrucción limpia. |
||
|
|
102aabd1ff |
SEGUNDA corrección: lo que medía el test era DERIVA, no no-determinismo — el corpus SÍ reproduce
El §1.bis dijo «sí hay no-determinismo» porque appstream y bison divergían. También estaba mal,
y por un fallo de diseño del propio test.
`verificar-repro.sh` compara el artefacto GUARDADO contra una reconstrucción de hoy. Pero el
guardado puede tener meses: se construyó con OTRO estado del lab. El test conflaba dos cosas:
· NO-DETERMINISMO — mismas entradas, mismo lab, salidas distintas (rompe el invariante);
· DERIVA — el artefacto viejo no es lo que el lab de hoy produce (el mundo se movió).
LA PRUEBA QUE LAS SEPARA es construir DOS VECES HOY, y se dio sin querer: al reejecutar el
verificador sobre recetas ya reconstruidas, TODAS pasaron a reproducir.
anew ✗ diverge → ✓ REPRODUCE
gron ✗ diverge → ✓ REPRODUCE
age ✗ diverge → ✓ REPRODUCE (y en la 1ª ni instalaba los mismos ficheros: faltaba age-inspect)
⇒ EL CORPUS ES DETERMINISTA HOY. Lo que hay es deriva contra artefactos viejos.
QUÉ SIGNIFICA, sin adornos:
· El argumento de reproducibilidad para el split de debug SE CAE. El split se justifica por
ESPACIO (~60%), 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, así que «nuestros artefactos son verificables por terceros» es
falso para parte del corpus — quien reconstruya no obtendrá lo publicado. `age` es el caso
feo: la reconstrucción ni siquiera instala los mismos ficheros.
· ⇒ La reconstrucción masiva SIGUE valiendo la pena, pero por otra razón: no para arreglar el
determinismo sino para PONER EL STORE AL DÍA CON EL LAB y que la promesa sea cierta.
Y otro hallazgo del mismo experimento: en el hub las recetas Go SÍ reconstruyen (0 fallos). Lo
que fallaba en el worker era la RED para bajar los módulos, no las recetas. Eso cambia el
bloqueante de la etapa 4: no es «Go no reconstruye», es «el worker no tiene red para módulos».
LA LECCIÓN, que es la misma tres veces en este documento: 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. El script queda anotado con esto en la cabecera.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
eea1c02f43 |
etapa 1: la puerta funcionó — me equivoqué, SÍ hay no-determinismo (y las dos mitades son una)
Ayer escribí en el SDD 23 que el `-ffile-prefix-map` global «se cancela porque su premisa es
falsa». **Era falso**, y la etapa 1 lo demostró en veinte minutos. Corregido en el §1.bis.
LA MEDIDA: `scripts/verificar-repro.sh` aparta el artefacto, reconstruye con las deps cacheadas
y compara con `why-differs`. Sobre las que pudieron reconstruirse: binutils ✓ y wl-clipboard ✓
reproducen; **appstream ✗ y bison ✗ DIVERGEN**. Causa idéntica en ambas:
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: mi barrido buscaba rutas DEL HOST (/home/<user>/,
/tmp/<aleatorio>) y no halló 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 encontrarla no
prueba que no haya otra. Y una sola muestra que reproduce no es una muestra: leí `wl-clipboard`
como si probara más de lo que probaba, que es el error contra el que el propio documento
advertía dos párrafos antes.
Lo que me salvó fue haber puesto la PUERTA antes de la campaña en vez de después. Si hubiera
seguido mi conclusión, habría cerrado como «resuelto» un invariante roto.
🎁 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 tocar -ffile-prefix-map en
720 recetas. Queda decidir qué hacer con el contenido del paquete -debug, pero eso afecta a un
artefacto secundario que nadie instala por defecto.
El script queda como herramienta: muestra por clase de build (C, Rust, Go: el no-determinismo
suele vivir en codegen paralelo y orden de símbolos, no sólo en C), y RESTAURA el artefacto si
el rebuild falla — distinguir «no reproduce» de «no construye acá» importa, porque mezclarlos
inventaría un problema de determinismo que no existe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|