Commit Graph
100 Commits
Author SHA1 Message Date
Sergio 85a393aff4 cadena wasm completa: wasi-libc y wasi-sdk sellan (SDD 26, unidad 3.b)
Los cuatro eslabones que enciende RLBox en firefox, sellados de punta a punta:

  wasi-libc-headers  b3:54c6e96d  (andamio, rompe el ciclo del bootstrap)
  wasi-compiler-rt   b3:a3fe63d1  (libclang_rt.builtins-wasm32.a)
  wasi-libc          b3:8e50b145  (libc.a 970.734 bytes, 32 archivos .a)
  wasi-sdk           b3:c8a587fa  (el .cfg que hace el sysroot invisible)

Los dos primeros ya estaban construidos; los dos últimos costaron dos fallos, y
ninguno de los dos era de la receta:

1. `check-symbols` moría por SESGO DE CLANG, no por un artefacto malo. El lab
   trae clang 22.1.8 y este commit de wasi-libc (2025-06-26) congeló su snapshot
   de macros predefinidos contra un clang anterior, así que el diff completo era
   UNA línea: `+#define __wasip1__ 1`. La mitad que importa —los símbolos
   definidos e indefinidos de la libc recién construida— coincidía byte a byte.
   Por eso se RECONCILIA `expected/*/predefined-macros.txt` en vez de saltar el
   chequeo con `make no-check-symbols`, que era la salida fácil: ese target
   existe y se lleva por delante también la comparación de símbolos, que es la
   que vale. Así cualquier OTRA deriva sigue matando el build.

2. `make TARGET_TRIPLE=NOBUILD ... install` NO es un no-op. La regla `install`
   depende de `finish`, que depende de `libc`, así que make no lee el triple
   inexistente como «no construyas» sino como «construí para el triple NOBUILD»
   y muere en `build/NOBUILD/.../crt1-command.o` con `invalid thread model
   'single'` — un error que nombra un fichero que nadie pidió. Se copia
   `sysroot/{lib,share,include}` a mano, que es literalmente lo que hace la
   regla de upstream (`SYSROOT ?= $(CURDIR)/sysroot`).

Cada receta lleva su guardián de contenido, porque acá el modo de fallo caro no
es el ausente sino el vacío: un sysroot con headers y sin libc.a pasa por bueno
y revienta dentro del build de firefox, horas después y sin nombrar a nadie de
esta cadena. Verificado en verde: los builtins son objetos wasm de verdad
(magic 0061736d, no x86_64 nativos) y el .cfg de wasi-sdk apunta a un sysroot
que existe.

El segundo fallo lo diagnosticamos entre dos sesiones: hammer-f8 aportó el
`invalid thread model 'single'` que explicaba la línea que yo veía.

Queda para la próxima unidad el flip de recipes/firefox.toml:
--without-wasm-sandboxed-libraries -> --with-wasi-sysroot, que es un rebuild
largo y su propia unidad de trabajo.
2026-09-06 01:29:41 +00:00
Sergio 78a7f87836 estado: cosecha granja 2026-09-06T01:02:16Z — avance del árbol KDE 2026-09-06 01:02:16 +00:00
Sergio 3f3653b171 estado: cosecha granja 2026-09-06T00:31:52Z — avance del árbol KDE 2026-09-06 00:31:52 +00:00
Sergio c0f94c93ac estado: cosecha granja 2026-09-06T00:02:19Z — avance del árbol KDE 2026-09-06 00:02:19 +00:00
Sergio 25cc429c9e estado: cosecha granja 2026-09-05T23:32:18Z — avance del árbol KDE 2026-09-05 23:32:18 +00:00
Sergio 0c552057e1 estado: cosecha granja 2026-09-05T23:02:27Z — avance del árbol KDE 2026-09-05 23:02:27 +00:00
SergioandClaude Opus 5 bf9f1e4ca3 atuq: firefox REPRODUCE — why-differs 56/56 sobre dos builds reales
La deuda de BuildID queda cerrada con la misma prueba que se le exigió al
derivado: apartar el artefacto como .ref, construir de nuevo y comparar.
56 entradas idénticas, 0 divergen. Antes de MOZ_BUILD_DATE esto no podía
salir bien, y build-state.json no lo veía porque el ArtifactHash es
input-addressed y no se mueve por la hora del build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011L4H7RabF2NCxCvFPJi37r
2026-09-05 22:48:02 +00:00
Sergio 98ded1dd92 estado: cosecha granja 2026-09-05T22:31:57Z — avance del árbol KDE 2026-09-05 22:31:57 +00:00
Sergio d4e2b8f1a7 estado: cosecha granja 2026-09-05T22:02:17Z — avance del árbol KDE 2026-09-05 22:02:17 +00:00
Sergio 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í.
2026-09-05 21:44:50 +00:00
Sergio 6c1f80204a repro: cobertura 92 de 1156 — la tanda sólo-C es la sostenible
18 recetas C verificadas de una, en unos nueve minutos, 15 REPRODUCEN y 3 eran DERIVA (ya al día).
Cero no-determinismos. Y `work/sources` se quedó en 4 KB toda la tanda gracias a la poda por
veredicto: antes una tanda así llevaba el disco del 95% al 98%.

Lo que hace sostenible a esta y no a la anterior es la SELECCIÓN. El filtro `MAX_SECONDS` sobre los
tiempos del worker no transfiere a Rust —`zola` se comió 40 minutos y no dio un solo veredicto— pero
en C sí: sin caché de cargo de por medio, un build de 40 s allá son 40 s acá. Lista explícita de
clase `c`, no muestreo ciego.
2026-09-05 21:41:41 +00:00
Sergio 2277d17b13 estado: cosecha granja 2026-09-05T21:32:15Z — avance del árbol KDE 2026-09-05 21:32:15 +00:00
Sergio 1505bfad78 licencias: ffmpeg y LLVM salen del árbol — «lo decide un humano» era «lo decide la receta»
Tres que había clasificado como decisión legal y no lo eran. Las dos causas son distintas y las dos
me las estaba perdiendo por mirar sólo el árbol:

**ffmpeg → LGPL-2.1-or-later.** Su raíz trae cuatro COPYING.* y por eso el detector se planta, con
razón: el árbol solo no puede decidir. Pero el `LICENSE.md` del propio ffmpeg explica la regla —«In
combination the LGPL v2.1+ applies to FFmpeg» y «None of these parts are used by default, you have to
explicitly pass `--enable-gpl`… In this case, FFmpeg's license changes to GPL v2+»— y la respuesta la
da NUESTRA receta: no pasa `--enable-gpl` ni `--enable-version3`, y encima va con
`--disable-autodetect`. O sea que la licencia sale de cruzar el árbol con los flags, no de elegir uno
de los cuatro ficheros.

**llvm18 y clang18 → Apache-2.0 WITH LLVM-exception.** Acá el detector veía «tres licencias
distintas» en un solo `LICENSE.TXT` y se plantaba, pero el fichero ABRE diciendo «The LLVM Project is
under the Apache License v2.0 with LLVM Exceptions»: lo demás es la sección legada. La regla de
«varias licencias ⇒ decide un humano» es correcta como default y aquí el humano sólo tuvo que leer la
primera línea.

`licenses/` pasa de 48 a 49 textos: `LLVM-exception` hacía falta para poder declararla, que es el
lazo de siempre —no se declara lo que no se puede entregar—. El único sin texto canónico sigue siendo
`LicenseRef-qorpa-ajena-no-enumerable`, que es identificador nuestro y SPDX no publica.

97% (1135/1165); la cola baja de 27 a 24. Los 3 ArtifactHash, idénticos.
2026-09-05 21:26:53 +00:00
Sergio 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.
2026-09-05 21:05:12 +00:00
Sergio 8b43b44c11 estado: cosecha granja 2026-09-05T21:02:03Z — avance del árbol KDE 2026-09-05 21:02:03 +00:00
Sergio e348c97964 aichat: era NO REPRODUCIBLE, y la causa estaba en el build.rs de un crate transitivo
`scripts/verificar-repro.sh` lo cazó en el worker: dos reconstrucciones seguidas, misma máquina y
mismo lab, dan `usr/bin/aichat` distintos. Es el primer no-determinismo REAL que el instrumento
encuentra —hasta hoy todo lo que había marcado era deriva— y salió porque el arreglo de esta tarde
conserva los dos ejemplares en `store/.divergen/` en vez de borrarlos.

`hammer why-differs` dijo `.rodata`. El primer byte que cambia (offset 1749687) cae dentro de un JSON
de *stopwords* por idioma embebido en el binario, y lo que difiere no es el contenido sino el ORDEN:
una build empieza por `"sl"` y la otra por `"fi"`.

De ahí a la causa: el crate es `stop-words` 0.8.1, transitivo vía bm25, y su `build.rs` arma un
`HashMap<String, Vec<String>>` y lo serializa con `serde_json::to_string`. El `HashMap` de Rust usa
`RandomState` —semilla ALEATORIA POR PROCESO— así que el orden de serialización cambia en cada
compilación y ese JSON se hornea en el binario. `BTreeMap` serializa en orden de clave. Es un arreglo
de una palabra que upstream no hizo.

Se parchea el crate vendorizado en una fase `configure` (el vendoreo ocurre ANTES de las fases, así
que el árbol ya está ahí) y se vacía la lista de ficheros de su `.cargo-checksum.json`, por la misma
razón y con la misma técnica que `recipes/firefox.toml`: cargo verifica el sha de cada fichero
vendorizado y no ofrece forma de recalcularlo.

Los `[ -f ]` fallan ruidoso a propósito. Si mañana la cadena de deps deja de traer stop-words, lo
peor sería que el parche se saltara en silencio y el binario volviera a ser aleatorio sin que nadie
se entere — que es exactamente cómo esto llegó hasta acá.

Re-hashea aichat (b3:c40a15bd → b3:7a515b0a), y eso es gratis: `yupana radio` da 0 dependientes y
ninguna imagen lo declara.
2026-09-05 20:49:23 +00:00
Sergio a2e5ebd6d7 estado: cosecha granja 2026-09-05T20:32:01Z — avance del árbol KDE 2026-09-05 20:32:01 +00:00
Sergio 058162c6d3 licencias: la cola de pendientes también se escribe — «lo que falta y por qué» es dato
Las 33 que quedan sólo existían en la salida del script, o sea que se perdían al cerrar la terminal.
Para quien tiene que decidirlas —y son decisiones legales, no mecánicas— «qué falta y POR QUÉ no se
puede afirmar» es tan dato como los veredictos. `docs/licencias-pendientes.tsv` las deja listadas con
su motivo: cuáles traen varios ficheros de licencia que dicen cosas distintas (gmp, ffmpeg, clang18,
llvm18, los `gi-*`), cuáles tienen un LICENSE que no reconozco y con qué frase empieza (fuse3, lsof),
y cuáles no traen nada en el árbol (boost, pigz, sqlite-shared).

Al revés que `licencias-evidencia.tsv`, ésta se REGENERA entera en cada corrida. No es un registro de
lo que se probó sino una foto de lo que queda, y una pendiente resuelta tiene que DESAPARECER de acá
—si se acumulara, la lista de trabajo mentiría hacia arriba para siempre.
2026-09-05 20:18:21 +00:00
Sergio 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.
2026-09-05 20:05:33 +00:00
Sergio 118ba2e2c3 estado: cosecha granja 2026-09-05T20:03:46Z — avance del árbol KDE 2026-09-05 20:03:46 +00:00
Sergio 4067bf817e estado: cosecha granja 2026-09-05T19:32:01Z — avance del árbol KDE 2026-09-05 19:32:01 +00:00
Sergio 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.
2026-09-05 19:31:33 +00:00
Sergio 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.
2026-09-05 19:03:48 +00:00
Sergio 5420d2db0d estado: cosecha granja 2026-09-05T19:02:04Z — avance del árbol KDE 2026-09-05 19:02:04 +00:00
Sergio 3ddac0269c SDD 26 §2.sexies: tres afirmaciones que el documento hacía sin medir, y cómo se cerraron
El re-empaque determinista, la capa de configuración y el branding visible eran afirmaciones, no
mediciones. Quedan con su prueba al lado: why-differs entre dos builds de atuq (85 idénticas, 0
divergen), el testigo escrito como pref de usuario, y la captura de la pantalla real.

De la primera salió el contraste que importa: el DERIVADO reproducía y la BASE no. El BuildID de
firefox era la hora del build, y build-state.json no lo podía ver porque el hash es input-addressed.
Verde y mintiendo. Cerrado, con guardián.

La regla que sale del día entero: cuando la duda es «¿llegó al artefacto?», la respuesta no está en
el log ni en la receta — está dentro del artefacto.
2026-09-05 18:39:35 +00:00
Sergio e0dce7c4b7 estado: cosecha granja 2026-09-05T18:33:07Z — avance del árbol KDE 2026-09-05 18:33:07 +00:00
Sergio d13af44ddb kernel: el contrato admite capacidades de INTERFAZ — el test llevaba rojo desde el 03-09
`cargo test` del workspace fallaba en `el_contrato_del_repo_cierra_sobre_si_mismo`:
«proceso-por-descriptor no declara símbolos». No es un descuido del alta: `abda7d3` dio de alta pidfd
con `symbols = []` y dedicó ocho líneas a explicar por qué va vacío — pidfd no depende de ningún
`CONFIG_*`, es interfaz del core desde Linux 5.3. Lo que no se actualizó fue el invariante, así que
el dato quedó bien y el test quedó rojo. Dos días sin que nadie lo viera, que es lo que pasa cuando
una suite falla por algo que «ya se sabe»: deja de mirarse entera.

El invariante correcto no es «toda capacidad tiene símbolos» sino **«toda capacidad afirma algo
comprobable»**: sin símbolos vale, pero entonces hay que nombrar la INTERFAZ. Sin una ni la otra, la
capacidad no dice nada que se pueda verificar y lo más probable es que sea un campo a medias — que
es el caso que el assert original quería atrapar y sigue atrapando.

Hoy hay exactamente una capacidad así y declara `interface = ["pidfd_open(2)", "pidfd_send_signal(2)"]`.

Workspace: 541 tests en verde, 0 fallos.
2026-09-05 18:17:26 +00:00
Sergio 59a0e56a16 licencias 97% (1132/1165): «no hay fichero» y «no reconozco el texto» no son lo mismo
El script decía «sin COPYING ni declaración en el árbol» para `lsof`, `when`, `wpa_supplicant`,
`perl-xml-parser` y `fuse3` — y las cinco SÍ tienen un COPYING o un LICENSE en la raíz. Lo que
pasaba es que ninguna de mis huellas reconocía su texto, que es un problema distinto y se acciona
distinto: uno pide buscar la licencia en otro sitio, el otro pide una huella nueva o leerla. Decir lo
mismo de los dos casos mandaba a mirar donde no era. Ahora el mensaje nombra el fichero y cita su
primera línea, y con eso tres se resolvieron leyéndolas:

  · `wpa_supplicant` — su COPYING no es una licencia sino un aviso: «the project has chosen to use
    only the BSD license option for future distribution», y remite al README. El README trae las tres
    cláusulas clásicas ⇒ BSD-3-Clause. Sin seguir esa remisión no había veredicto posible.
  · `when` — «under the same terms as Perl, or, at your option, under version 2 of the GPL». «Los
    mismos términos que Perl» ES la disyunción Artistic-1.0-Perl OR GPL-1.0-or-later, y la opción de
    GPL-2 ya queda cubierta por el `-or-later`.
  · `perl-xml-parser` — su LICENSE es el texto íntegro de la Artistic License 2.0.

Ese último obligó a cerrar un lazo que yo mismo había dejado abierto: el script se niega a declarar
un SPDX cuyo TEXTO no tengamos en `licenses/`, porque la obligación legal es entregar el texto y no
el nombre. Pero `licencias-textos.sh` deriva qué textos bajar DE las licencias ya declaradas ⇒ una
licencia genuinamente nueva no podía entrar por ningún lado. El orden que lo resuelve es: declarar
con evidencia, bajar el texto, comprobar que llegó. `licenses/` pasa de 44 a 48 textos y el único que
queda sin texto canónico es `LicenseRef-qorpa-ajena-no-enumerable`, que es un identificador nuestro y
SPDX no publica.

Quedan 33. `lsof` (licencia propia de Purdue) y `fuse3` (aviso compuesto: LGPL para la librería, GPL
para las utilidades) siguen pendientes a propósito, y el resto son expresiones que decide una
persona: gmp, ffmpeg, clang18, llvm18 y los `gi-*` traen VARIOS ficheros de licencia que dicen cosas
distintas, y en ffmpeg la respuesta ni siquiera está en el árbol —la deciden los flags de la receta.

Los 3 ArtifactHash, idénticos antes y después.
2026-09-05 18:10:52 +00:00
Sergio b87835f719 licencias 96% (1129/1165): la fuente git también es evidencia, y el script deja de llamarse «tarball»
Faltaban tres recetas cuya fuente es un repo, no un tarball: `fcft`, `foot` y `glab`. El commit
pineado cumple exactamente el mismo papel que el sha256 —es el árbol EXACTO que construimos— así que
la evidencia estaba igual de disponible, sólo que por otra puerta.

`miembros_de_git()` la abre sin clonar el árbol entero: `--depth 1 --filter=blob:limit=64k` sobre el
commit. Un COPYING nunca pasa de 64k y los fuentes donde vive la concesión tampoco; los que sí pasan
—binarios, assets— son justo los que no interesan. Un repo de cientos de megas se resuelve con unos
pocos. Las tres salieron del `license:` que el propio autor escribe en su `meson.build` (fcft, foot)
y del texto del LICENSE (glab).

Los repos por SSH se saltan diciéndolo: este script hace fetch anónimo y no tiene claves. Son
`hammerd` y `portal-probe`, y ya están resueltas por otra vía (su repo es este mismo).

`analizar()` pasa a consumir un iterable de `(ruta, bytes)` en vez de abrir el tar ella misma, así
que las dos fuentes comparten TODA la lógica de veredicto: la jerarquía de evidencia, la regla de
sólo-la-raíz, la unión de identificaciones y la exigencia de que la concesión hable de la misma
versión que el COPYING. Un criterio, dos puertas.

Y el script se renombra `licencias-tarball.py` → `licencias-fuente.py`, porque el nombre viejo pasó a
ser mentira en el momento en que aprendió a leer git. Un nombre que describe la implementación de
ayer manda a la gente a buscar en el sitio equivocado.

Los 3 ArtifactHash, idénticos antes y después.
2026-09-05 18:07:17 +00:00
Sergio 9e6e3d5aa6 estado: cosecha granja 2026-09-05T18:02:52Z — avance del árbol KDE 2026-09-05 18:02:52 +00:00
Sergio 5e489e974e firefox: BuildID determinista — se cierra la deuda de reproducibilidad, con guardián
Medido y probado hoy: `atuq` REPRODUCE bit a bit (`why-differs`: 85 entradas idénticas, 0 divergen),
lo que valida el re-empaque determinista del omni.ja, el XPI, los iconos copiados y el BuildID
derivado del contenido. El que NO reproducía era la BASE: el `BuildID` de firefox era la fecha y
hora del build (`20260905035306` en el artefacto sellado), así que dos construcciones con el mismo
ArtifactHash daban bytes distintos.

Es el agujero exacto que el invariante de reproducibilidad existe para cerrar, y `build-state.json`
no lo puede ver: el hash es input-addressed y no se mueve por esto. Verde y mintiendo.

El valor sale de `SOURCE_DATE_EPOCH`, que el sandbox de hammer YA fija a 1 para todas las recetas —
mejor que una constante escrita a mano, porque usa el mecanismo de hermeticidad que ya existe en vez
de inventar un segundo. Alpine hace lo mismo en su APKBUILD. BuildID esperado: 19700101000001.

Va en LAS TRES FASES, por la lección que costó 50 minutos esta misma tarde con MOZ_REQUIRE_SIGNING:
son shells distintos y `mach build` re-ejecuta el configure cuando le parece.

Y con su GUARDIÁN, hermano del de la firma: la fase install compara el BuildID del artefacto contra
el que SOURCE_DATE_EPOCH obliga, y falla el build si no coinciden. Los dos fallos de esta familia
—flag que el configure acepta y el artefacto ignora— son mudos y viajan lejos; la única defensa es
comprobarlos donde importa, que es dentro del artefacto.
2026-09-05 17:37:30 +00:00
Sergio db7fd81236 parches: los 8 FUZZ del catálogo, mirados uno por uno — y un libro que se auto-invalida
`vigia-parches.py --all` sobre las 99 aplicaciones de parche del catálogo: **0 FALLA**, y 14 líneas
FUZZ (8 pares parche/fuente distintos). FUZZ significa que `patch` metió el cambio ADIVINANDO dónde
porque el contexto no casaba, y si cayó en el sitio correcto sólo lo dice el diff. Nadie los había
mirado. Los miré todos:

  · **libxml2 / CVE-2026-6732** — el que más importaba, un parche de seguridad con fuzz 2 en sus dos
    hunks. Cayó bien: los dos dentro de `xmlParseReference()`, las 4 llamadas pasan `ctxt->userData`
    y no sobrevive ningún `sax->characters(ctxt,` en el fichero. El fuzz era por un offset de 226
    líneas, no por el sitio.
  · **doas / rowhammer** — el otro sensible, toca la decisión de privilegio. Cae dentro de
    `checkconfig()` y queda `rv=permit(...); if(rv==0)→permit`, coherente con el `if(rv!=0)→EPERM`
    de `main()`. Y el fuzz lo causa un parche ANTERIOR de la propia cadena (el `#ifdef DOAS_CONFDIR`
    que inserta `configuration-directory.patch`), no un cambio de upstream — que es una causa que no
    se me habría ocurrido sin abrirlo.
  · **wayland**, **mesa** (×3), **cairo** (×2), **firefox** (time64 y fix-rust-target): todos en su
    sitio, cada uno comprobado contra lo que el propio parche declara querer.
  · **gnupg / 0001-include-unistd** — hallazgo: el parche está OBSOLETO. Añade `#include <unistd.h>`
    y upstream YA lo trae dos líneas más abajo, así que sólo lo duplica. Inocuo (el header tiene
    guardas) y por eso el fuzz 2: cambió el contexto porque upstream lo incorporó. Quitarlo re-hashea
    gnupg, así que se paga cuando se re-selle por otro motivo.

Y para que esto no se repregunte en cada corrida —lo que vuelve ruido al vigía, y así es como se
pierde el FALLA del día que aparezca— los veredictos van a `docs/state/fuzz-verificado.tsv` con un
cuarto estado, FUZZ✓.

La clave del libro NO es (receta, parche) sino (receta, parche, HUELLA), donde la huella resume el
texto del parche MÁS el pin de la fuente. Tocá el parche o subí la versión y la huella cambia, la
entrada deja de casar y el vigía vuelve a preguntar. Es lo contrario de una lista de excepciones: no
hay forma de silenciar algo y que siga silenciado cuando cambió. Y el vigía imprime la línea lista
para pegar debajo de cada FUZZ sin verificar, porque calcular la huella a mano es justo la fricción
que hace que nadie lo anote.

Los dos FUZZ de waterfox quedan FUERA del libro a propósito: no los verifiqué en esta ronda y
waterfox está fuera de alcance. Van a seguir saliendo como FUZZ, que es lo honesto — el libro dice
lo que se miró, no lo que se supone.
2026-09-05 17:34:28 +00:00
Sergio 2a62c44b19 estado: cosecha granja 2026-09-05T17:32:08Z — avance del árbol KDE 2026-09-05 17:32:08 +00:00
Sergio f6e690d994 vigia-imagen: un módulo QML registrado en C++ no está «sin instalar» — se comprueba, no se recuerda
Los DOS únicos ✗ que reportaba este vigía eran falsos, y de la misma clase: un `import` no siempre se
satisface con un directorio y su `qmldir`. El greeter de la pantalla de bloqueo hace
`qmlRegisterUncreatableType<PamAuthenticator>("org.kde.kscreenlocker", …)` — el módulo existe SÓLO
dentro del proceso que carga ese `.qml` y en disco no hay nada que instalar. Buscarlo en el sistema
de ficheros da ✗ con la imagen perfecta.

Los dos, verificados uno por uno antes de tocar el script:
  · `org.kde.kscreenlocker` ← `usr/lib64/libexec/kscreenlocker_greet`. Y valía la pena mirarlo: los
    que lo importan son `lockscreen/LockScreenUi.qml` y `MainBlock.qml` de plasma-desktop, o sea la
    pantalla de bloqueo de verdad. Si hubiera faltado no era un diálogo de ajustes.
  · `org.kde.newstuff.core` ← `usr/lib/qt6/qml/org/kde/newstuff/libnewstuffqmlplugin.so`.

Antes esto se tapaba anotando el módulo a mano en `QML_EN_RUNTIME`. `registrado_en_binario()` lo
contesta ahora con evidencia —el URI queda como literal en `.rodata`, así que se busca en los ELF del
cierre— y además NOMBRA al registrador, que es lo que permite auditar el veredicto sin repetirlo.
Mira primero los artefactos cuyo nombre comparte una palabra con el URI, que es donde está casi
siempre, y sólo si no aparece recorre el resto.

⇒ la lista escrita a mano SE BORRA ENTERA. Al vaciarla, los cuatro módulos que tenía se resolvieron
solos, cada uno nombrando su binario (plasmashell, kwin_wayland). Y salió gratis un hallazgo que
ninguna lista podía dar: `org.kde.kwin.effect` no se importa en NINGÚN cierre — era una entrada
muerta, y nadie tenía cómo saberlo.

Los cinco perfiles quedan en ✓ de qml, sin un solo ✗ en todo el informe.

La regla detrás: un guardián que grita en falso no se endurece, se ignora. Cuando el 100% de sus
hallazgos son falsos positivos, el trabajo no es anotarlos — es enseñarle a preguntar bien.
2026-09-05 17:11:23 +00:00
Sergio 52d3bbfeb3 atuq-nested: CAPTURA=<png> — la prueba en el píxel, en una variable
Durante horas dije «mirá tu pantalla» y reporté «el proceso vive» como si fuera lo mismo. No lo es:
la ventana estaba ahí desde el principio y lo que fallaba era el branding DENTRO del zip, que
ningún log iba a contar.

`grim` ya estaba en el corpus (`incoming-wlr`), se hidrata en el mismo rootfs y captura la pantalla
real contra el mismo socket wayland desde dentro de la jaula. Ahora es parte del runner: con
`CAPTURA=/ruta/foto.png` se lanza atuq, se espera a que pinte y se deja el PNG afuera para mirarlo.

De paso, la resolución de raíces prueba el corpus Y `incoming-wlr`, en ese orden — el mismo que usa
la resolución de deps (sibling-first, luego el catálogo padre). Sin eso, `grim` no se encontraba y
el script habría dicho «falta un artefacto» sobre una receta que existe.
2026-09-05 17:06:17 +00:00
Sergio 5c8d5ba956 estado: cosecha granja 2026-09-05T17:02:10Z — avance del árbol KDE 2026-09-05 17:02:11 +00:00
Sergio 2660063f99 atuq v0.4: el branding que se VE estaba en las imágenes del zip, no en el texto
Capturé la pantalla con `grim` y por fin miré lo que el usuario miraba. El diagnóstico cambió
entero: la ventana SÍ estaba —el título decía «New Tab — atuq» y el icono del lanzador era el
zorro— pero la PÁGINA mostraba el logo y la palabra «Nightly».

O sea que la v0.2 rebrandeó lo que se lee y no lo que se ve. El branding de Gecko vive en TRES
sitios y sólo uno es texto FTL:

  · `brand.ftl`        — las cadenas nuevas (título de ventana, menús). Ya estaba.
  · `brand.properties` — las cadenas VIEJAS, formato heredado. Seguía diciendo Nightly, y nadie lo
                         habría notado leyendo la receta: son 71 bytes en otra ruta del mismo zip.
  · las imágenes de `chrome/browser/content/branding/` — TODO lo visible: el favicon de las páginas
                         internas (ese orbe azul en la pestaña), el logo de about:, y el «wordmark»,
                         que es una IMAGEN con la palabra dibujada, no un texto traducible.

Se reemplazan 10 imágenes + las dos tablas de cadenas + los dos wordmarks. Cada imagen va con LAS
MISMAS DIMENSIONES que la que sustituye (192, 384, 300x236, 16/32/48/64/128): son tamaños que el CSS
del chrome da por sentados, y meter otro tamaño descoloca la página en vez de rebrandearla. El
`about.png` (300x236, no cuadrado) lleva el medallón ENCAJADO y centrado, no estirado.

El wordmark conserva el `viewBox` exacto del original (0 0 372 99) y `fill="context-fill"`, que es
la convención de Gecko para que el color lo ponga quien lo usa — así sigue el tema claro/oscuro solo
en vez de imponer un color.

La variante «private» usa el mismo medallón a propósito: inventar una segunda marca para un modo del
navegador es ruido, y ese modo ya se distingue por su propio chrome.

MÉTODO, que es lo que más vale de esta vuelta: `grim` está en el corpus (incoming-wlr), se hidrata
en el mismo rootfs y captura la pantalla real desde dentro de la jaula. Dejé de decir «mirá tu
pantalla» y pasé a mirarla yo.
2026-09-05 16:45:58 +00:00
Sergio 17a7eda1c7 sqlite-shared: anotada la verruga del sqlite3 de CLI, con el costo de las dos salidas
El binario de la CLI sale con `NEEDED libreadline.so.8` y el `readline` canónico del corpus es `.a`,
así que en la imagen GNOME ese comando no carga — lo resolvía contra el sysroot del lab. Las apps no
están afectadas: el NEEDED lo lleva `usr/bin/sqlite3`, no `libsqlite3.so.0`.

No lo arreglo de paso porque las dos salidas cuestan cosas distintas y la elección no es de rutina:
una `readline-shared` de raíz es 1 build pero suma una librería a la imagen por un comando;
`--disable-readline` acá es más limpio pero `yupana radio` da 6 dependientes que caen a deuda.

La nota va en la receta y no en un documento aparte porque es donde alguien la va a editar.
Verificado que un comentario no toca el ArtifactHash (b3:22e25fc7 antes y después).
2026-09-05 16:40:56 +00:00
Sergio 3a83d2eda4 gnome: xz-shared — sin ella NINGUNA app de la imagen carga, no una
`scripts/vigia-sonames.py` sobre los cinco perfiles marcaba `liblzma.so.5` en GNOME pedido por
`libadwaita` y por `python3`. La columna de QUIÉN lo pide es la que decide, y acá decidió fuerte:
mirando el artefacto sellado, el `NEEDED` no lo lleva un binario suelto sino
**`usr/lib/libadwaita-1.so.0`**, que es la librería contra la que enlaza toda app GTK4/GNOME de la
imagen. Sin proveedor del SONAME el loader falla en TODAS, no en una.

Misma fuga de siempre —el `xz` canónico del corpus es `.a`, así que el rootfs lo resolvía contra el
sysroot Alpine DEL LAB— y por eso invisible al store: el lab no entra en `hash_inputs`.

La receta `-shared` existía sólo en `incoming-kde`. Se promueve al corpus con el mismo procedimiento
que `bzip2-shared` el 2026-09-03, y comprobando lo mismo antes de creerlo: el hash es idéntico desde
las dos rutas (b3:174289d5) ⇒ un solo artefacto y cero rebuilds. Si hubiera diferido, sería otra
receta y no valdría la promoción.

Va sólo en GNOME: en cosmic y sway ese soname lo pide únicamente `python3`, que es herramienta de
build y no viaja en la imagen.

GNOME pasa de 3 sonames sin proveedor a 2. De los dos que quedan, `libperl.so` es de `perl` (build),
y `libreadline.so.8` lo pide `usr/bin/sqlite3` —el shell de la CLI, NO `libsqlite3.so.0`—, así que
las apps están bien y lo roto es el comando. Queda anotado como verruga, no como rotura.
2026-09-05 16:39:06 +00:00
Sergio 92d47cfe1d mirada: las tres -shared que faltaban — el USB resolvía expat/zlib/libffi contra el lab
`scripts/vigia-sonames.py` sobre los cinco perfiles: `escritorio-mirada` era el único que aún tenía
la fuga al sysroot del lab en componentes de RUNTIME. `mesa-swrast` y `wayland` salen con
`NEEDED libexpat.so.1`, `libz.so.1` y `libffi.so.8`; las recetas canónicas de expat, zlib y libffi
son `--disable-shared`; ningún artefacto del cierre publicaba esos SONAME. O sea que el rootfs los
resolvía contra el Alpine DEL LAB y el USB sólo arrancaba donde hubiera Alpine debajo — que es peor
que una dep faltante, porque el lab no entra en `hash_inputs` y el store no puede ni notarlo.

Es la misma fuga que los otros cuatro perfiles cerraron el 2026-09-03. mirada quedó fuera porque su
lista nació como copia literal del PKGS de `mirada-usb.sh` y nadie la revisó desde entonces. La
dirección hoy está invertida —el script deriva su PKGS de `targets.py escritorio-mirada`— así que
arreglarlo acá arregla el USB, y no hay dos listas que puedan divergir. Actualicé el comentario, que
seguía diciendo «lift verbatim».

Las tres viven en `corpus`, la misma cola del perfil, y ya están selladas ⇒ CERO rebuilds: sólo
entran a la clausura.

NO sumo `bzip2-shared` ni `ncurses-shared`, que sí llevan los otros perfiles: acá esos sonames los
pide únicamente `python3`, que es herramienta de build y no viaja en la imagen. El vigía imprime
siempre QUIÉN pide cada soname justamente para poder separar eso; sin esa columna su informe no se
puede triar y uno acaba tapando ruido.

Verificado: mirada pasa de 10 sonames sin proveedor a 7, y los 7 que quedan son todos de `python3` y
`perl`.
2026-09-05 16:37:02 +00:00
Sergio 058c5231dc estado: cosecha granja 2026-09-05T16:32:14Z — avance del árbol KDE 2026-09-05 16:32:14 +00:00
Sergio 748b2f604c yupana: el khipu se descubre, no se escribe a mano — cosmic y wlr eran invisibles
Dos sitios llevaban la lista de grafos codificada como `(kde, gnome, corpus)` y **omitían
`build-state-cosmic.json` y `build-state-wlr.json`**, que son dos de los cuatro escritorios. Es
exactamente la forma del fallo que motivó este fichero y que su cabecera describe: medir contra un
subconjunto de las colas y que el número salga chico sin que nada avise.

Medido: `_estado()` devolvía `?` para los 36 nudos propios de cosmic y para 12 de los 14 de wlr.

Pero el daño peor no era el `?`. `_estado()` elegía el fichero por la cola —gnome si
`incoming-gnome`, corpus si `corpus`, **KDE para todo lo demás**— así que un nudo de `incoming-wlr`
se buscaba en el grafo de KDE. `gdk-pixbuf` existe en `corpus` Y en `incoming-wlr` con recetas y
hashes distintos, y la consulta por el de wlr devolvía el estado del de corpus, con toda confianza.
Coincidía en `sealed`, así que nunca se notó. Un `?` se ve; una respuesta de la receta equivocada,
no.

Ahora `_khipus()` los descubre por glob (un grafo nuevo entra solo; ninguno se puede olvidar) y
`_estados_por_cola()` indexa por **(cola, nombre)**, que es la clave real: el mismo nombre vive en
varias colas siendo recetas distintas. Si la cola no tiene ese nudo, la respuesta es `?` — que es la
verdad, y no el estado de su homónimo.

`keystones` compartía el mismo índice por nombre suelto: pasa a `(cola, nombre)` en las tres
consultas.

Los tres invariantes de `test-yupana-radio.py` siguen verdes, y las 50 discrepancias de cosmic+wlr
bajan a 0.
2026-09-05 16:31:31 +00:00
Sergio 66db079b64 firefox: la bandera de firma en LAS TRES fases, y un guardián que lo comprueba en el artefacto
El build anterior selló con `MOZ_REQUIRE_SIGNING: true` DENTRO del omni.ja, pese a que el configure
aceptó la variable sin una queja. Cincuenta minutos para descubrirlo, y no al construir sino al
intentar instalar la extensión de la página de inicio — a dos recetas de distancia y con un error
(`ERROR_SIGNEDSTATE_REQUIRED`) que no nombra la bandera por ningún lado.

LA CAUSA ES LA QUE ESTA MISMA RECETA YA DOCUMENTABA PARA OTRA VARIABLE: las fases son shells
distintos y `mach build` re-ejecuta el configure cuando lo cree necesario. Por eso `RUST_TARGET` se
repite en las tres desde siempre; `MOZ_REQUIRE_SIGNING` estaba sólo en `configure`.

Y como el fallo es de los que viajan lejos y callados, la fase install ahora lo COMPRUEBA donde de
verdad importa —el `AppConstants.sys.mjs` que va dentro de omni.ja— y falla el build si no quedó en
`false`, con el mensaje que dice qué revisar. Un flag que el configure acepta y el artefacto ignora
es justo la clase de cosa que sólo se ve si alguien la mide.

Lo que se leyó para llegar acá, y no se dedujo: el parser de mozbuild confirma que el valor VACÍO
desactiva (`if values == ("",): return NegativeOptionValue()`) y que un env vacío sí se considera
presente (`if env is not None`). O sea que el mecanismo era correcto; lo que faltaba era que la
variable estuviera donde el build la vuelve a leer.
2026-09-05 16:25:39 +00:00
Sergio b1a0b3323e estado: cosecha granja 2026-09-05T16:02:07Z — avance del árbol KDE 2026-09-05 16:02:07 +00:00
Sergio ab7cdf25e3 licencias: hammerd y portal-probe son MIT — su repo es el propio hammer
Las dos recetas apuntan a `ssh://gitea@git.gioser.net:2345/sergio/hammer.git`, o sea a este mismo
árbol, que declara MIT en `Cargo.toml` y trae su `LICENSE`. No hace falta clonar nada para saberlo:
la evidencia estaba en el directorio de trabajo. Los dos ArtifactHash, idénticos.
2026-09-05 15:44:55 +00:00
Sergio 96855001c6 licencias: 96% (1124/1164) — --fetch para los tarballs que no estaban en caché
Ocho paquetes más (giflib, pcre2, nghttp2, libogg, libvorbis, speexdsp, libuv, tea) con la misma
regla de evidencia. Su tarball no estaba en `work/tarballs/`, así que el modo `--fetch` lo baja a un
temporal, lo verifica contra el sha256 QUE LA RECETA PINEA —o es el árbol exacto o no se mira— y lo
borra. No se escribe en la caché de hammer a propósito: es suya, y un fichero puesto ahí por otro
camino es una vía de envenenamiento que nadie audita.

Dos correcciones al criterio, las dos porque produjo una afirmación falsa:

  · `gmp` salía GPL-3.0-or-later. Su `COPYING` es la GPLv3, pero al lado trae `COPYING.LESSERv3` y
    `COPYINGv2` porque la biblioteca es LGPL. La regla anterior —«el `COPYING` a secas gana al
    sufijado»— arregla socat, cuyo `COPYING.OpenSSL` es una excepción, y rompe gmp, donde el extra
    nombra otra VERSIÓN y no una dep. Esa diferencia es demasiado fina para codificarla sin
    equivocarse, así que ahora se identifican TODOS los ficheros de la raíz y sólo hay veredicto si
    dicen lo mismo. gmp, ffmpeg y los tres `gi-*` quedan pendientes, que es lo correcto: en ffmpeg
    la respuesta ni siquiera está en el árbol, la deciden los flags de la receta.

  · Pero «varios ficheros» no es «ambigüedad»: el `COPYING` de pcre2 son dos líneas apuntando a
    `LICENCE.md`, y los dos dicen BSD-3. Por eso se unen las identificaciones en vez de contar
    ficheros — ambiguo es que digan cosas DISTINTAS.

Y el TSV pasa a ser ACUMULATIVO. Una receta ya sembrada sale del conjunto de entrada porque ya tiene
`license`, así que regenerar el fichero entero borraba su cita — y la cita es el rastro de
auditoría, lo único que deja revisar un veredicto sin repetir el trabajo. Un fichero de evidencia
que se olvida de lo que ya probó no es evidencia.

Los 10 ArtifactHash afectados, idénticos antes y después.
2026-09-05 15:43:51 +00:00
Sergio b606bf988b targets.toml: atuq entra en los cuatro escritorios
Hasta ahora atuq estaba sellado y NO estaba en ninguna imagen. Es exactamente la lección que este
fichero ya aprendió con `foot` en el perfil de sway: una receta sellada que ningún perfil declara no
la lleva nadie, y la métrica de clausura no lo puede ver porque mide lo DECLARADO.

Va en los cuatro por el mismo motivo que `mpv` y desde el mismo sitio: vive en el CORPUS, y una
receta resuelve sibling-first y después el catálogo padre, así que las cuatro colas lo alcanzan.

Arrastra la cadena GTK3 en sus variantes `-shared`, y el comentario lo dice: con las estáticas,
libgtk-3 y libgdk-3 se llevaban cada una su copia de pango/cairo y el navegador no llegaba a pintar.

NOTA sobre la métrica: el grafo del hub sólo reporta base/cli/escritorio-mirada — los cuatro
escritorios no aparecen en `by_profile` porque sus raíces viven en las colas. Es previo a este
cambio y no lo introduce; queda anotado porque significa que esta declaración NO se ve todavía en
`build-state.json`.
2026-09-05 15:36:08 +00:00
Sergio ba1fb2d3a8 licencias: 95% (1114/1164) — la evidencia sale del tarball pineado, offline
`licencias.sh --sembrar` escribe desde una tabla curada a mano y `licencias-desambiguar.sh` resuelve
`-only` vs `-or-later` preguntándole a la búsqueda de código de GitHub. Los dos dejan fuera lo mismo:
lo que nadie curó y lo que no vive en GitHub.

Pero `work/tarballs/` indexa cada tarball por su sha256, así que el árbol EXACTO que la receta pinea
ya está en disco: la declaración del propio autor, en el commit que construimos, sin red. De las 76
sin licencia, 55 tenían su tarball cacheado y 25 salieron con evidencia citable.

`scripts/licencias-tarball.py` la busca en tres niveles: la declaración del autor (`Cargo.toml`,
`meson.build`), un único fichero en `LICENSES/` (REUSE, que usa KDE), y el texto del COPYING más la
CONCESIÓN buscada en las cabeceras de los fuentes — excluyendo COPYING/LICENSE, porque el apéndice
de la GPL trae literalmente «or (at your option) any later version» y buscarla ahí da siempre
positivo siendo plantilla. Es la regla que ya fijó `licencias-desambiguar.sh`, aplicada al árbol
pineado en vez de a GitHub.

La primera versión resolvía 40, y CUATRO estaban mal. Las dejo escritas porque son la forma del
error, no accidentes:

  · `socat` salía BSD-2 por su `COPYING.OpenSSL`, que es la excepción, no la licencia. Un `COPYING`
    a secas gana ahora a cualquier sufijado.
  · `pigz` salía Apache-2.0 por `zopfli/COPYING`: la licencia de una pieza VENDORIZADA leída como la
    del contenedor. Sólo se mira la raíz.
  · `openssh` salía MIT porque su `LICENCE` es un compendio de cuatro y me quedaba con la primera
    que pegara. Si el texto trae varias, no hay veredicto: lo compone un humano.
  · `nano` salía GPL-3.0-**or-later** citando el «either version 2» de su `aclocal.m4` — plantilla de
    autotools, no del proyecto, y encima de otra versión. Ahora la cita tiene que venir de un fichero
    del autor Y hablar de la misma versión mayor que el COPYING.

Y dos bugs míos que producían el mismo daño en silencio: la marca de BSD-3 era «Neither the name of»,
que libpcap y libzip no usan («The names of the authors may not be used to endorse») ⇒ se declaraban
BSD-2; y el mayor de versión lo sacaba de `"GPL-3.0".rsplit("-",1)[0][-1]`, que da «L», así que la
comprobación de coherencia rechazaba TODA cita válida.

La validación final no es una regex: un identificador vale si tenemos su texto en `licenses/`. La
obligación legal es acompañar el binario del TEXTO, así que un SPDX que no podemos entregar no
adelanta nada y sí crea una afirmación que no se sostiene. Eso es lo que atrapa el «GPL2+» que meson
deja escribir en `gsd-schemas` y `libgdm`, que no es un SPDX sino taquigrafía.

Nada se sembró a ojo: cada línea de `docs/licencias-evidencia.tsv` lleva la cita que la decidió, y
las 50 que quedan salen listadas con lo que SÍ se encontró en vez de rellenarse.

Comprobado que no se invalida nada: los 26 ArtifactHash afectados son idénticos antes y después
(`license` no está en `hash_inputs`) — medido, no supuesto.
2026-09-05 15:34:35 +00:00
Sergio 33f7c37312 SDD 26 §2.quinquies: la v0.3 destapó que la unidad 6 dependía de ésta sin decirlo
Tres mecanismos medidos para poner una página de inicio: `distribution/extensions/` no instala nada
(sideloading retirado, y el fallo es MUDO), `policies.json` sí se lee y falla nombrándose, y la pref
de firma no alcanza porque la exigencia viene compilada.

Lo que destrabó el diagnóstico fue un TESTIGO: `defaultPref` no deja rastro en prefs.js, así que un
autoconfig que no corre es indistinguible de uno que corre y no hace nada. Con una pref de usuario
escrita desde el .cfg se pudo separar las dos hipótesis sin abrir el navegador.

Y el hallazgo que cambia el plan: sin `MOZ_REQUIRE_SIGNING` vacío, el `sct` del §6.1 —el
diferenciador que nadie más tiene— tampoco se podría shipear nunca. La unidad 6 colgaba de ésta y el
documento no lo decía.
2026-09-05 15:33:54 +00:00
Sergio eb20e1d91e estado: cosecha granja 2026-09-05T15:33:38Z — avance del árbol KDE 2026-09-05 15:33:38 +00:00
Sergio 37afec380f firefox: MOZ_REQUIRE_SIGNING se desactiva con el valor VACÍO, no con cero
El primer intento murió en un segundo:

    mozbuild.configure.options.InvalidOptionError: MOZ_REQUIRE_SIGNING takes 0 values

Es una bandera booleana: el `0` no es «falso», es un VALOR que la opción no acepta. La semántica no
se dedujo, se leyó del parser de mozbuild sacado del propio tarball
(`python/mozbuild/mozbuild/configure/options.py`):

    if values == ("",):  return NegativeOptionValue()   ⇒ VAR=   (vacío) DESACTIVA
    if values == ("1",): return PositiveOptionValue()   ⇒ VAR=1  activa

Y el `export` con valor vacío es justo lo que hace llegar la variable PRESENTE y vacía: desexportarla
la dejaría sin definir y volvería al default, que acá es exigir firma porque el default sale de
`milestone.is_release_or_beta and not milestone.is_esr` — 154.0 es release.

Queda escrito en la receta con las dos líneas del parser, porque «=0 no es falso» es la clase de
detalle que cuesta una hora la segunda vez que aparece.
2026-09-05 15:25:48 +00:00
Sergio 1dc662c48d atuq v0.3: página de inicio y pestaña nueva — y la firma de complementos venía COMPILADA
La página de inicio va en una extensión de sistema (`inicio@atuq.tawasuyu`) y no en una pref, porque
la pestaña NUEVA no se puede redirigir con prefs desde hace años: el único mecanismo soportado es
`chrome_url_overrides.newtab`. La misma pieza resuelve la home con `chrome_settings_overrides`.

La página: el medallón, el nombre y una barra que decide sola. Si lo tecleado PARECE una dirección,
navega; si no, BUSCA CON EL MOTOR POR DEFECTO DEL USUARIO vía `browser.search.query`. No se cablea
ningún buscador: una distro que hornea su motor en la página de inicio está tomando por el usuario
la única decisión que esa página debería respetar. Y dice cuál es el motor, para que la búsqueda no
sea una caja negra.

DOS MECANISMOS MUERTOS Y UNO VIVO, TODOS MEDIDOS:
- `distribution/extensions/` (el clásico de las distros) NO instala nada: Firefox retiró el
  sideloading. El `extensions.json` del perfil ni lo mencionaba y el log no decía una palabra —
  fallo perfectamente silencioso.
- `policies.json` con `ExtensionSettings` SÍ se lee (`browser.policies.applied=true` en el perfil) y
  falla con un error que al menos se nombra: `ERROR_SIGNEDSTATE_REQUIRED`.
- Y `xpinstall.signatures.required=false` TAMPOCO alcanza.

PARA SABER SI EL AUTOCONFIG SIQUIERA CORRÍA, HIZO FALTA UN TESTIGO. `defaultPref` no deja rastro en
`prefs.js` (sólo se guarda lo que difiere del default), así que un `.cfg` que no se ejecuta es
INDISTINGUIBLE de uno que sí y no hace nada. `atuq.cfg` ahora escribe `atuq.autoconfig.ok` como pref
de USUARIO, legible desde fuera sin abrir el navegador. Salió `true` ⇒ el .cfg corre, y la pref de
firma se estaba ignorando: la exigencia viene COMPILADA. El default de `MOZ_REQUIRE_SIGNING` sale
del MILESTONE (154.0 es release), no del canal — el nuestro es `default` y aun así estaba activa.

De ahí `export MOZ_REQUIRE_SIGNING=0` en firefox.toml, con su precio escrito: este Firefox deja de
exigir la firma de Mozilla para CUALQUIER complemento. La alternativa es firmar en AMO —cuenta y
revisión de Mozilla por versión—, que es la dependencia externa que esta distro existe para no
tener. Y no es un desvío: sin esto, el `sct` del SDD 26 §6.1 tampoco se podría shipear nunca.

La prueba se hizo SIN reconstruir: se montó el `.cfg` con el testigo por encima del artefacto con
`--ro-bind`, en vez de editar un hardlink del store (que habría corrompido el artefacto sellado).
2026-09-05 15:11:37 +00:00
Sergio 114aeba514 fetch: materializa los submódulos git, que git archive no exporta
`fetch_git` clona a un mirror y materializa el árbol con `git archive | tar -x`, a propósito, para
no pagar un worktree. Pero `git archive` NO emite nada por una entrada gitlink (modo 160000): un
árbol con submódulos salía INCOMPLETO y el fallo aparecía recién en la fase configure, minutos
después y hablando de un fichero "que no existe".

Por eso un `--recurse-submodules` en el clone no arreglaba nada: el que los pierde es el archive,
no el clon. Lo que hace falta es leer `.gitmodules` + los SHA de gitlink DEL COMMIT, mirrorear cada
submódulo y archivarlo en su subruta, recursando. Encaja con el ADR 0006 sin ceder determinismo: un
submódulo ya viene pineado por SHA en el commit del padre.

Dos detalles que no son obvios:

  · La URL se reescribe SSH→HTTPS. `.gitmodules` suele declarar `git@github.com:X/Y.git` y nuestro
    fetch es anónimo, sin claves. Apunta al mismo repo y el commit está pineado, así que el
    contenido no puede diferir: no añade nada que verificar. Las relativas (`../l10n.git`) se
    resuelven contra la URL del padre tratada como DIRECTORIO, que es lo que hace git — `..` se
    come el nombre del propio repo, no el del directorio que lo contiene.

  · `.gitmodules` se lee del COMMIT (`git config --blob`), no del working tree, porque no hay
    working tree. Y los gitlinks se listan con `ls-tree -z`: sin `-z` git escapa las rutas con
    espacios entre comillas.

El test reproduce el caso completo con repos locales: comprueba primero que sin esto el gitlink
queda como directorio VACÍO —el síntoma exacto que costó el diagnóstico— y después que con esto el
fichero del submódulo llega.
2026-09-05 15:11:35 +00:00
Sergio 6a5408adae estado: cosecha granja 2026-09-05T15:05:08Z — avance del árbol KDE 2026-09-05 15:05:08 +00:00
Sergio 082246da84 atuq: la marca de verdad — el zorro en medallón, y la paleta muestreada del propio logo
El operador pasó un tablero de identidad: zorro en medallón con cenefa andina, disco teal, anillo
noche. Se adapta la FORMA y el COLOR. Los eslóganes NO — los descartó explícitamente, y de todos
modos `brand.ftl` no es sitio para copy: lleva el nombre y nada más.

EL ICONO DEJA DE DIBUJARSE EN CÓDIGO. Hasta la v0.2 `rebrand.py` generaba una marca geométrica en
Python puro, con el argumento de que un PNG en el árbol no se puede revisar en un diff. Ese
argumento valía mientras el icono era un marcador de posición inventado por falta de arte. Ya hay
arte, así que el criterio se invierte: un asset de diseño ES la fuente, no un derivado. Lo que se
conserva es la disciplina — los seis tamaños salen del MISMO master (recorte del medallón con
máscara circular y alfa, LANCZOS desde 1024 px), se generan una vez y se copian, así que dos builds
ponen los mismos bytes.

LA PALETA SE MIDIÓ, NO SE ESTIMÓ. Los cuatro tokens salen del color más frecuente de cada zona del
medallón: #c15728 el zorro, #1b6470 el disco, #04161d el anillo, #f5ecde los reflejos. Escribirlos
«parecidos» a ojo es cómo una identidad se desalinea de su propio logo en tres ediciones. Con eso el
CSS suma `accent-color` —un solo cambio alcanza decenas de widgets nativos del chrome sin enumerar
ninguno— y el fondo de la barra de pestañas verticales, para que marco e icono cuenten lo mismo.

⚠ Anotado y no escondido: a 16 px el medallón pierde el detalle y queda como una mancha cálida
redonda. Es intrínseco a un logo con cenefa; la salida sería un dibujo simplificado para ese tamaño
y lo decide quien dibujó el zorro, no este script.

Construido y sellado EN LOCAL en 3 segundos (b3:97328d1c), hidratado y abierto: 8 procesos vivos,
anunciándose como `(atuq:9581)`.
2026-09-05 14:42:18 +00:00
Sergio 9f79362201 estado: cosecha granja 2026-09-05T14:32:59Z — avance del árbol KDE 2026-09-05 14:32:59 +00:00
Sergio 5d06a3b83d atuq ABRE: el app_id estaba compilado en el binario, y el árbol copiado llegaba de sólo lectura
Con la cadena GTK3 arreglada, atuq arrancó de verdad: parent vivo, CERO procesos de contenido
muertos, y pintando una página local. Quedaban dos cosas.

1. SE ANUNCIABA COMO `firefox-default`. El application.ini decía RemotingName=atuq y aun así el
   proceso se presentaba así — porque esa cadena es un MOZ_APP_REMOTINGNAME horneado en el ELF, y es
   la que Gecko pasa a g_set_prgname(), o sea el **app_id de Wayland**. Consecuencia visible: el
   escritorio no casaba la ventana con nuestro .desktop (StartupWMClass=atuq) ⇒ icono genérico y sin
   agrupar.

   Se parchea EN SITIO, no recompilando: cambiarlo en recipes/firefox.toml brandearía la BASE como
   atuq, y firefox tiene que seguir siendo firefox para los otros forks. Es seguro porque es una
   cadena C terminada en NUL en el pool de .rodata —`firefox-default\0` son 16 bytes exactos y se
   escriben 16—, y se EXIGE una única ocurrencia: si upstream la duplica o la renombra, falla en vez
   de parchear el sitio equivocado. Verificado después: 0 ocurrencias de la vieja, y el log dice
   `(atuq:26423)`.

2. UN BUG QUE SÓLO SE VE FUERA DE ROOT. `cp -a` preserva los modos y los ficheros del store están
   sellados sin permiso de escritura; todo lo que viene después los modifica. En el worker pasaba
   inadvertido porque corre como root, que ignora los bits. El primer build local murió con
   `PermissionError: /out/usr/lib/atuq/application.ini`. Se arregla con `chmod -R u+w` en la receta y
   no en el entorno: un artefacto no debe depender de con qué uid lo construiste.

Y el runner pasa a hidratar las variantes `-shared`: en runtime una `.a` no sirve de nada, y son las
que gtk3 declara NEEDED desde el arreglo del cuadro de las dos Pango.

Este atuq se construyó EN LOCAL en segundos, que era la promesa entera del diseño derivado del
SDD 26: iterar el envoltorio sin volver a pagar un build de Gecko.
2026-09-05 14:31:35 +00:00
Sergio 5db1c7ec89 estado: cosecha granja 2026-09-05T14:02:07Z — avance del árbol KDE 2026-09-05 14:02:07 +00:00
Sergio 4b9f8aa392 estado: cosecha granja 2026-09-05T13:32:14Z — avance del árbol KDE 2026-09-05 13:32:14 +00:00
Sergio 323475a6f1 estado: cosecha granja 2026-09-05T13:02:07Z — avance del árbol KDE 2026-09-05 13:02:07 +00:00
Sergio af0e90ba8d estado: cosecha granja 2026-09-05T12:32:10Z — avance del árbol KDE 2026-09-05 12:32:10 +00:00
Sergio a828e74c40 la cadena GTK3 se enlazaba estática dentro de dos .so: cuatro variantes -shared y xkeyboard-config al corpus
Con `atk` arreglado, atuq llegó más lejos y murió igual, ahora con Pango:

    GLib-GObject-CRITICAL: cannot register existing type 'PangoFontMap'
    … decenas de líneas … tipo '<invalid>'

MISMO CUADRO, CULPABLE DISTINTO Y MÁS GRANDE. `gtk3` produce DOS objetos compartidos —libgtk-3.so y
libgdk-3.so— y libxul declara NEEDED las dos, así que se cargan siempre juntas. Con las variantes
ESTÁTICAS de sus deps, cada una se llevaba adentro su propia copia. Medido con el mismo instrumento
que cazó a atk, `nm -D --defined-only`, preguntando quién DEFINE cada símbolo:

    pango_font_map_get_type → libgdk-3.so, libgtk-3.so, libgailutil-3.so
    cairo_create            → libgdk-3.so, libgtk-3.so
    gdk_pixbuf_get_type     → libgdk-3.so, libgtk-3.so
    hb_buffer_create        → libgdk-3.so, libgtk-3.so, libgailutil-3.so

Y se comprobó el otro lado: libxul NO embebe ninguna —enlaza libgtk-3/libgdk-3 como debe—, así que
la duplicación es toda interna del par GTK3.

Cuatro variantes nuevas: harfbuzz-shared, cairo-shared, gdk-pixbuf-shared, pango-shared. Sólo cambia
el modo de librería; se conservan todos los switches del canónico para no arrastrar deps nuevas.

DOS DEUDAS ANOTADAS, NO OLVIDADAS: `fribidi` y `pixman` siguen estáticos —no existe variante— pero
quedan embebidos en UNA sola .so cada uno, así que no hay copia que colisione; y `libepoxy` sí queda
en las dos, y se deja porque no registra tipos de GObject ni mantiene estado global. Si algún día
otra .so del mismo proceso los embebe, vuelve el cuadro.

`firefox` swapea las mismas cuatro: declarar las ESTÁTICAS junto a un gtk3 que trae las compartidas
son dos artefactos peleando por el mismo `pango.pc`, que es la otra forma conocida de este fallo.

Y `xkeyboard-config` SUBE AL CORPUS. Existía idéntica en las cuatro colas de escritorio (un solo md5
entre las cuatro, verificado antes de mover) y atuq, que vive en el corpus, no podía alcanzar
ninguna: sibling-first y después el catálogo padre, nunca una cola hermana. Sin sus datos el
navegador ni pinta («xkbcommon: failed to add default include path /usr/share/X11/xkb»). Mismo hash
en el corpus que en las colas ⇒ cero rebuilds, y las copias de las colas se quedan donde están.
2026-09-05 12:02:24 +00:00
Sergio 0bda91a374 estado: cosecha granja 2026-09-05T12:02:20Z — avance del árbol KDE 2026-09-05 12:02:20 +00:00
Sergio d2527284b9 atuq-nested: el rootfs se revalida por HASH, no por existencia
El script daba por bueno el rootfs si el directorio estaba: «¿ya está hidratado?». Es la pregunta
equivocada. Tras el re-hash de la cadena atk→gtk3→firefox→atuq el directorio seguía ahí con los
artefactos VIEJOS, así que habría abierto contento la versión que acabábamos de arreglar y el
diagnóstico habría sido buscar en el sitio equivocado. Misma forma del cache-hit que congela
regresiones.

Ahora resuelve los hashes vigentes, los anota en `.raices` y rehidrata cuando difieren.

De paso: nada de `diff <(...)`, que es de bash. El shebang dice /bin/sh y un script que sólo anda
cuando /bin/sh resulta ser bash es una trampa que salta en otra máquina.
2026-09-05 11:52:18 +00:00
Sergio 00910be94d SDD 26 §2.quater: abrirlo en una pantalla destapó tres fallos que la clausura no veía
Con atuq sellado y verificado, se hidrató y se abrió en waypipe. No arrancó, y los tres motivos son
justamente la diferencia entre «sellado» y «usable»: el EXDEV de hidratar a otro mount, el lanzador
que no puede ser un symlink porque los ELF no traen RPATH, y un bug del CORPUS —atk se había tragado
GObject entero por declarar la glib estática siendo una .so—.

El tercero es el que vale escribir: el síntoma (45 GLib-GObject-CRITICAL y un SIGSEGV) no nombra a
atk por ningún lado, y no se ve mirando el rootfs porque había UNA sola libgobject. Se caza
preguntando quién DEFINE el símbolo, no quién lo usa.

Y una corrección de número: firefox se reconstruye en ~50 minutos, no en las cuatro horas que este
documento venía repitiendo. Ese número era folclore de la receta, no una medición.
2026-09-05 11:47:51 +00:00
Sergio 9250f07a56 estado: cosecha granja 2026-09-05T11:01:56Z — avance del árbol KDE 2026-09-05 11:01:56 +00:00
Sergio 14fbb42834 estado: cosecha granja 2026-09-05T10:31:58Z — avance del árbol KDE 2026-09-05 10:31:58 +00:00
Sergio 6d8bb7d901 estado: cosecha granja 2026-09-05T10:02:14Z — avance del árbol KDE 2026-09-05 10:02:14 +00:00
Sergio 0f7d19232e atuq: el lanzador deja de ser un symlink, y un runner para abrirlo en la pantalla que ya tenés
Dos cosas que salieron de intentar ABRIRLO, que es lo único que distingue «sellado» de «usable».

1. EL LANZADOR. Con `/usr/bin/atuq` como symlink, el navegador moría antes de pintar:

     XPCOMGlueLoad error for file /usr/lib/atuq/libmozsandbox.so:
     Error loading shared library libnspr4.so: No such file or directory

   El motor carga sus propias librerías desde `/usr/lib/atuq` y ni el binario ni esas `.so` traen
   RPATH/RUNPATH — comprobado con `readelf -d`, no supuesto. Pasa a ser un script que exporta
   `LD_LIBRARY_PATH` (lo mismo que hacen Debian y Fedora) y hace `exec`, para que el proceso que
   queda sea el motor y `/proc/self/exe` siga resolviendo el appdir. El `LD_LIBRARY_PATH` además
   tiene que HEREDARSE: Firefox lanza un proceso por pestaña y todos cargan las mismas librerías.
   La alternativa limpia es grabar RPATH=$ORIGIN con `patchelf` —es lo que hace Alpine— pero
   `patchelf` todavía no existe como receta del corpus; cuando exista, esto vuelve a ser un symlink.

2. `scripts/atuq-nested.sh` — hidrata el cierre de runtime y abre atuq como ventana anidada en el
   compositor que ya está delante (waypipe, mirada, sway). Trae dos cosas aprendidas a golpes:

   · EL ROOTFS VA EN EL MISMO MOUNT QUE EL STORE. `hydrate` proyecta con hardlinks y `linkat()`
     rechaza cruzar un punto de montaje aunque sea el mismo filesystem. Acá el store es /dev/sdb
     bind-monteado y `work/` vive en /dev/sdc: hidratar a `work/…` muere con «Invalid cross-device
     link (os error 18)». El volumen entero está en /mnt/cosecha, así que el rootfs va ahí. Se
     comprueba con `findmnt -T`, nunca con `stat -c %d`.
   · `dejavu-fonts` va en las raíces por necesidad, no por completismo: un navegador sin una sola
     fuente arranca, pinta y muestra cuadraditos. Ya nos costó una tarde en GNOME.

   Y no saltea en silencio: si falta un artefacto de la lista, sale con error en vez de armar un
   rootfs al que le faltan tres paquetes y falla tres capas más abajo.
2026-09-05 09:53:33 +00:00
Sergio c9c6727c3b atk: a las variantes -shared — su .so llevaba una copia entera de GObject adentro
Al abrir el primer atuq en pantalla, murió así:

    GLib-GObject-CRITICAL: cannot register existing type 'gpointer'
    … 45 líneas iguales … Segmentation fault (exit 139)

El síntoma no nombra a atk por ningún lado, y las dos glib no estaban como dos ficheros en el
rootfs —había UNA sola libgobject—. La segunda copia estaba EMBEBIDA: `atk` declaraba la variante
ESTÁTICA de glib y produce un objeto compartido, así que el enlazador le metió GObject entero
dentro de `libatk-1.0.so`. Todo proceso que cargue atk y libgobject a la vez —o sea cualquier app
GTK— acaba con dos sistemas de tipos peleando.

SE ENCONTRÓ MIDIENDO, NO LEYENDO: `nm -D --defined-only` sobre cada `.so` del rootfs, buscando
quién DEFINE `g_type_register_static`. Dos respuestas: `libgobject-2.0.so`, que debe, y
`libatk-1.0.so`, que no.

Es la regla que el corpus ya tenía escrita desde GTK4 y que esta receta se saltó: las deps de una
`.so` van a las variantes `-shared` EN LUGAR DE las estáticas, nunca junto a ellas.

RADIO MEDIDO ANTES DE TOCAR (`yupana radio atk`): 4 transitivos, 3 sellados caen a deuda —
atk, gtk3 y firefox. GNOME NO está en el radio: usa gtk4. El costo es un rebuild de firefox de
cuatro horas, y no hay forma de esquivarlo: sin esto el navegador no abre.

Esto es «sellado ≠ usable» otra vez, y la única razón por la que apareció es que alguien lo ABRIÓ
en una pantalla. La clausura decía 100%.
2026-09-05 09:53:17 +00:00
Sergio 901ed3ea02 estado: cosecha granja 2026-09-05T09:32:20Z — avance del árbol KDE 2026-09-05 09:32:20 +00:00
Sergio 9d0ed92b60 estado: cosecha granja 2026-09-05T09:02:08Z — avance del árbol KDE 2026-09-05 09:02:09 +00:00
Sergio 1728fd65e5 firefox NO reproduce bit a bit: el BuildID es la fecha del build — medido y anotado, no arreglado de paso
Al abrir `application.ini` para el branding de atuq apareció esto:

    BuildID=20260905035306

Es la fecha y hora del build. Dos construcciones de la MISMA receta, con el mismo ArtifactHash,
producen bytes distintos. Es justo el agujero que el invariante de reproducibilidad existe para
cerrar, y no se ve en `build-state.json` porque el hash es input-addressed y no se mueve por esto.

El arreglo es conocido y lo usa Alpine: exportar `MOZ_BUILD_DATE` con un valor determinista.

NO se aplica de paso, a propósito: vive en una FASE, así que re-hashea la receta y cuesta otro build
de cuatro horas más el re-sellado de atuq, que cuelga de ella. Es su propia unidad de trabajo y
queda como 4.c en el plan del SDD 26.

El comentario se comprobó que NO mueve el hash antes de commitearlo (b3:aa90192a antes y después):
un comentario fuera de las fases no entra en `hash_inputs`, así que el firefox sellado sigue válido.

Y el §2.ter del SDD queda con el estado real de atuq v0.2 y su evidencia.
2026-09-05 08:50:17 +00:00
Sergio 6f6c32bb9d atuq 0.2.0: branding real — el zip, el .ini, los binarios y un icono que no es de otro
Hash b3:7eff4ba4. La v0.1 dejó el branding fuera porque había que MIRAR el árbol antes de adivinar.
Se miró, y lo que apareció decidió la forma de esta versión:

- `application.ini` del appdir arranca diciendo «This file is not used». Es herencia del árbol de
  desarrollo: en un build empaquetado el launcher SÍ lo lee. `rebrand.py` no le cree al comentario:
  comprueba que las claves estén y falla si no.
- Las cadenas que la gente VE están DENTRO de `browser/omni.ja` ⇒ sin re-empacar el zip no hay
  branding. Y el original no dice «Firefox», dice **Nightly**, porque construimos con
  `--with-branding=unofficial`. Sin tocarlo, atuq se presentaría como lo peor de los dos mundos.
- Los ICONOS, en cambio, viven FUERA del zip ⇒ se reemplazan sin abrirlo.

EL RE-EMPAQUE PRESERVA TRES PROPIEDADES DEL ORIGINAL, Y CADA UNA POR UNA RAZÓN MEDIDA:
orden de las 5306 entradas (es el que Gecko lee al arrancar, y es lo que el jarlog del PGO va a
refinar), `compress_type=0` (Mozilla lo deja SIN COMPRIMIR para poder mapearlo) y `date_time`
2010-01-01 (ya venía normalizado: Mozilla también persigue reproducibilidad).

VERIFICADO CONTRA EL ARTEFACTO, NO CONTRA EL LOG:
- diff entrada por entrada entre el omni.ja de firefox y el de atuq: **5306 → 5306, mismo orden,
  todo STORED, mismas fechas, y UNA SOLA entrada con contenido distinto**: brand.ftl.
- re-empacar dos veces el mismo zip da el mismo sha256 ⇒ el paso es determinista.
- application.ini: Vendor=tawasuyu, Name/RemotingName/CodeName=atuq. `ID` NO se toca: es el GUID
  con el que las extensiones declaran compatibilidad, y cambiarlo dejaría a atuq fuera del
  ecosistema de complementos.

EL BuildID SE DERIVA DEL CONTENIDO DEL OVERLAY, no de la fecha. Es la clave con la que Gecko
invalida su startup cache: si atuq cambia su chrome y el BuildID no se mueve, el navegador arranca
con la interfaz vieja cacheada y PARECE que el overlay no agarró. Derivarlo del contenido lo mueve
exactamente cuando hace falta y dos builds del mismo atuq dan el mismo número — con la fecha
pasaría lo contrario en los dos sentidos. Se vio funcionar: al sumar `CodeName` el BuildID cambió
solo, de 81921491609672 a 24128696592342.

El icono se DIBUJA en código (PNG en Python puro, zlib + struct) y no se commitea como binario:
un PNG en el árbol no se puede revisar en un diff, el código sí, y el resultado es idéntico en cada
build. ⚠ Es un marcador de posición declarado —una marca geométrica plana, 5 tamaños—; lo correcto
es que alguien dibuje el zorro. Su única función irrenunciable ya la cumple: que la ventana y el
lanzador NO muestren el icono de otro producto.

Binarios: firefox → atuq y firefox-bin → atuq-bin, con symlink `firefox` → `atuq` dentro del appdir
porque hay scripts de terceros que invocan por el nombre histórico y Gecko resuelve su directorio
por /proc/self/exe (entrar por el symlink resuelve al mismo sitio). Más `.desktop` con
StartupWMClass=atuq, que casa con el app_id que da RemotingName bajo Wayland.
2026-09-05 08:49:46 +00:00
Sergio 4843756757 estado: cosecha granja 2026-09-05T08:32:09Z — avance del árbol KDE 2026-09-05 08:32:09 +00:00
Sergio fcc99aabfb estado: cosecha granja 2026-09-05T08:02:07Z — avance del árbol KDE 2026-09-05 08:02:07 +00:00
Sergio 03f442de11 estado: cosecha granja 2026-09-05T07:32:10Z — avance del árbol KDE 2026-09-05 07:32:10 +00:00
Sergio e15be575ab estado: cosecha granja 2026-09-05T07:01:53Z — avance del árbol KDE 2026-09-05 07:01:53 +00:00
Sergio 3a0e7e7fbf estado: cosecha granja 2026-09-05T06:32:06Z — avance del árbol KDE 2026-09-05 06:32:06 +00:00
Sergio fb83528fed estado: cosecha granja 2026-09-05T06:02:10Z — avance del árbol KDE 2026-09-05 06:02:10 +00:00
Sergio 8ce3db0053 estado: cosecha granja 2026-09-05T05:32:08Z — avance del árbol KDE 2026-09-05 05:32:08 +00:00
Sergio a4af37e01d estado: cosecha granja 2026-09-05T05:01:58Z — avance del árbol KDE 2026-09-05 05:01:58 +00:00
Sergio f32fcb15d9 estado: cosecha granja 2026-09-05T04:32:12Z — avance del árbol KDE 2026-09-05 04:32:12 +00:00
Sergio 6068c4088d SDD 26 §2.ter: la fuente de atuq vive en el repo, y eso exigió un modo de [source] nuevo
El documento daba por hecho que la receta derivada se podía escribir con lo que hammer ya tenía.
No: `[source]` era obligatoriamente git o tarball. Queda escrito el muro, por qué los rodeos eran
peores (fetchear una fuente que se ignora miente sobre la identidad; un repo aparte obliga al worker
a leer algo privado) y la salida — `source.dir`, hasheado por contenido con el `of_tree` que ya
existía para Stage 2.

Y el estado real del plan: la unidad 4 tiene su v0.1 escrita, con el branding y el re-empaque de
omni.ja separados como 4.b porque los dos necesitan mirar el artefacto que `firefox` todavía no
selló.
2026-09-05 04:13:47 +00:00
Sergio aeebb194dc atuq 0.1.0: la receta del envoltorio, derivada de firefox
Primera receta del navegador de la distro (SDD 26). No compila nada: depende de `firefox`, copia su
árbol y le pone encima cuatro ficheros. Hash b3:2d6e4dcf.

LA CAPA SON CUATRO FICHEROS Y CADA UNO ESTÁ DONDE ESTÁ POR UNA RAZÓN:
- `defaults/pref/autoconfig.js` — el único gancho que corre ANTES de que exista un perfil.
- `atuq.cfg` — prefs de fábrica + carga del chrome. Con `defaultPref` y no `lockPref`: son valores
  de arranque, no una cárcel; lo que de verdad se bloquea va en policies.json.
- `chrome/atuq.css` — el aspecto.
- `distribution/policies.json` — telemetría, updates, primer arranque.

POR QUÉ EL CSS NO ES `userChrome.css`: ese fichero vive en el PERFIL y exige que el usuario prenda
`toolkit.legacyUserProfileCustomizations.stylesheets`. atuq tiene que verse como atuq en el primer
arranque, con un perfil recién creado, sin que nadie prenda nada. El único gancho a nivel de
APLICACIÓN es nsIStyleSheetService desde autoconfig — y llegar a él es la razón de
`sandbox_enabled = false`. Se registra como USER_SHEET, el mismo nivel de cascada que userChrome,
para que el usuario pueda seguir pisándolo.

DOS TRAMPAS DEL FORMATO, ESCRITAS EN EL PROPIO FICHERO:
- la PRIMERA LÍNEA de un autoconfig se ignora siempre, por diseño (herencia de Netscape). Código en
  la línea 1 no corre y no da error: el fallo más caro de ese fichero es el que no dice nada.
- el CSS va acotado con @-moz-document a browser.xhtml. Una hoja USER sin acotar alcanza CUALQUIER
  documento chrome —visor de PDF, inspector, diálogos— y un selector como `toolbar` termina pintando
  ventanas que nadie miró.

`extensions.autoDisableScopes = 0` es la otra mitad del `--with-unsigned-addon-scopes` que ya viaja
en el build de firefox: sin los dos, las extensiones de la distro se instalan DESACTIVADAS.

LAS ASERCIONES VAN PRIMERO Y FALLAN RUIDOSAS. Un derivado que no encuentra su base produciría un
directorio con cuatro ficheros de config, `Store::has` lo daría por presente y el fallo aparecería
el día que alguien abra el navegador (regla 3 del CLAUDE.md). Y al final imprime el inventario de la
capa con sus tamaños, que es la primera pregunta del diagnóstico cuando «atuq se ve como Firefox».

LO QUE LA v0.1 NO HACE, A PROPÓSITO: no renombra el binario ni toca application.ini, y no
re-empaqueta omni.ja. Las dos cosas necesitan mirar la forma real del árbol que selle `firefox`, y
adivinarla desde acá daría un artefacto que arranca en una máquina y en ninguna otra.

No se puede construir todavía: su dep `firefox` está en vuelo.
2026-09-05 04:12:55 +00:00
Sergio 54fa4a941c hammer: source.dir — el modo de fuente que faltaba para las recetas DERIVADAS
Al ir a escribir `recipes/atuq.toml` apareció un muro que el SDD 26 no había visto: `[source]` era
obligatoriamente git o tarball (`Source::kind` no tiene tercera salida), así que una receta cuyo
contenido no viene de upstream sino de NOSOTROS —un envoltorio sobre otro artefacto, un tema, una
configuración— no se podía ni escribir. Los rodeos posibles eran todos peores: fetchear una fuente
upstream que después se ignora MIENTE sobre la identidad del artefacto, y colgar el overlay de un
repo aparte obliga a que el worker tenga acceso de lectura a un repo privado.

`dir = "atuq"` apunta a un árbol dentro del propio repo, relativo al directorio de la receta.

SE HASHEA POR CONTENIDO, NO POR RUTA. `ArtifactHash::of_tree` ya existía (lo usa Stage 2 para
verificar bit-reproducibilidad) y hace exactamente lo que hace falta: rutas ordenadas, bit de
ejecución, contenido, sin seguir symlinks. Es la misma disciplina que ya tenían los `patches`, que
entran al hash por bytes y no por nombre. Comprobado a mano: editar un CSS del overlay mueve el
ArtifactHash y revertirlo lo devuelve exacto.

`dir` es EXCLUYENTE con repo/tarball y se comprueba primero. Declarar las dos cosas no es una
ambigüedad para resolver por precedencia: es un error de quien escribió la receta, y decirlo antes
del fetch evita bajar algo que después se pisa.

Los cuatro sitios que hacían match sobre `SourceKind` se cierran a mano y no con un `_`:
- `swm.rs` (×2) y `swm_bridge.rs` (×2): un `.swm` es un manifiesto COMPARTIBLE y necesita un puntero
  que el otro lado pueda resolver (commit o sha256). El árbol de una derivada vive en este repo y no
  hay puntero que mandar ⇒ error explícito en vez de emitir un manifiesto con el source vacío, que
  viajaría bien y rompería del otro lado. Error y no `unreachable!`: esto es librería, y un panic
  mataría al llamador por una receta mal escrita.
- `hammer pin`: una derivada ya está anclada por contenido ⇒ no hay ref flotante que fijar, lo dice
  y sale con 0.
- `hammer` → file_drop: mismo tratamiento que un source que no se puede expresar.

Dos tests: que `dir` resuelve y excluye a los otros dos, y que el mensaje de «source vacío» nombra
los TRES modos — ese texto es la única guía de quien escribe una receta a mano, y si sumamos un modo
sin tocarlo mandamos a la gente a buscar un campo que no existe.

⚠ Al correr la suite aparece un fallo AJENO a esto y que NO toqué:
`kernel::contract::tests::el_contrato_del_repo_cierra_sobre_si_mismo` — «proceso-por-descriptor no
declara símbolos», del frente kernel (commit abda7d3, alta de pidfd). Queda anotado para su dueño.
2026-09-05 04:12:34 +00:00
Sergio 52e4dbe8cc estado: cosecha granja 2026-09-05T04:02:19Z — avance del árbol KDE 2026-09-05 04:02:19 +00:00
Sergio 6dbe17f87d firefox: fuera clang18 y llvm18 — una dep del corpus tapaba el llvm-ar del lab
Primer build con clang+ThinLTO: murió a los 4 minutos, y el error nombra las dos versiones, que es
lo que lo hace diagnosticable de un vistazo:

    /usr/bin/llvm-ar: error: …log.o: 'Unknown attribute kind (102)'
                             (Producer: 'LLVM22.1.8' Reader: 'LLVM 18.1.8')

`clang18` y `llvm18` estaban en [deps].build de cuando esta receta compilaba con gcc: aportaban el
libclang para bindgen y la suite llvm-ar/llvm-objdump que Mozilla exige. Con `compiler = "clang"` se
volvieron ACTIVAMENTE DAÑINAS: una dep del corpus se materializa en el sandbox y TAPA el binario del
lab, así que llvm-ar pasó a ser el de LLVM 18 con un compilador clang 22. Mientras los .o eran
objetos no se notaba; con ThinLTO son BITCODE, y el bitcode no es compatible hacia atrás.

Misma forma que la lección de los headers UAPI: una dep tapa al lab y el fallo aparece lejos del
sitio que lo causó.

El arreglo no es apuntar AR a mano, es no traer el 18: el lab ya expone
/usr/bin/llvm-{ar,objdump,profdata} → llvm22 y /usr/lib/libclang.so → llvm22 (paso 3a-ter del
bootstrap). Es además lo que hace Alpine: clang22 + clang22-libclang, sin un segundo LLVM en la mesa.
Y llvm-profdata del 22 es el que va a hacer falta para el PGO.

Nuevo ArtifactHash: b3:aa90192a141f9fc6d2b59acd074db6caa36854af7100fb08fb51c8fe441b72b7
2026-09-05 03:51:39 +00:00
Sergio 442a319499 lab re-pineado con lld (d1e341d5) — y el sha «determinista» lo ensuciaba un log
Al ir a re-pinear la imagen para propagar el `lld` a la granja, hub y worker empaquetaron a shas
DISTINTOS. La imagen se creó para que el sha VERIFIQUE, así que un desacuerdo ahí no se pinea: se
mide.

Los dos rootfs son idénticos: 9550 entradas con la misma lista de ficheros, los mismos tamaños, y
CERO diferencias de metadato (modo, nlink, destino de symlink) en las 8778 entradas de fichero y
symlink. La única diferencia de contenido en todo el rootfs era `var/log/apk.log`, donde apk anota
la FECHA de cada operación — o sea que dos labs equivalentes empaquetaban distinto sólo porque las
instalaciones ocurrieron en momentos distintos. Un sha que no se puede reproducir desde un rootfs
equivalente no verifica nada, que es justo lo que este fichero existe para dar.

Se excluye ese log del tar (no todo `var/log`: cambio mínimo; apk lo recrea solo). Con la exclusión,
hub y worker dan el MISMO tar: 179d04f054f28d8dafe7c626d6b8c686a33a00a96715ee544a9bcf6f62cf586d.

⇒ LA GRANJA YA ESTABA SINCRONIZADA Y AHORA SE PUEDE DEMOSTRAR, sin correr `--traer` en un worker que
está construyendo firefox — reemplazarle el rootfs a mitad de build habría sido la forma cara de
descubrir lo mismo.

Nueva imagen publicada: hammer/lab/lab-d1e341d5….tar.zst (310 M), pin actualizado en
bootstrap-devfs.sh. Queda en el comentario cómo comparar dos máquinas: se compara el TAR y no el
`.tar.zst`, porque el sha comprimido depende de la versión de zstd de cada máquina y dos labs
idénticos con zstd distintos darían un falso desacuerdo.
2026-09-05 03:49:56 +00:00
Sergio b38e121a6b estado: cosecha granja 2026-09-05T03:32:02Z — avance del árbol KDE 2026-09-05 03:32:02 +00:00
Sergio fe940887bf waterfox aparcada: no falló por los parches, le falta browser/locales en el clon
Primer intento en el worker: murió en CUATRO SEGUNDOS, no en cuatro horas. `mach build` abortó con
«FATAL ERROR PROCESSING MOZBUILD FILE» sobre browser/moz.build porque referencia
browser/locales/moz.build y ese fichero NO ESTÁ en el árbol traído.

Lo que eso significa, y es la parte que vale: LOS ONCE PARCHES DE MUSL SÍ AGARRARON. La apuesta
explícita de esta receta —reusar los parches de 154 sobre un árbol 153.1.0— no es lo que falló. Lo
que falla es el FETCH: falta un trozo del árbol. Sospechoso número uno el `--filter=blob:none`
contra cómo BrowserWorks compone su repo, no la receta.

Se aparca por decisión del usuario: la prioridad pasa a firefox con la cadena de optimización y a
atuq (SDD 26). El primer paso al retomarla es un `git ls-tree` del commit pineado, no un build.
2026-09-05 03:21:23 +00:00
Sergio e5bf8da35a SDD 26 §3: corregido — la toolchain no era una receta de LLVM, era apk add lld
El documento estimó una receta `llvm-toolchain` (clang+lld+libc++ desde fuente) como la unidad de
mayor palanca del frente. Mirar el lab en vez de suponerlo la redujo a un `apk add`: clang22 y
llvm22 ya estaban, `Compiler::Clang` ya estaba cableado en hammer y ninguna receta lo usaba.

Se corrige el §3 con la evidencia y la medición de la huella (no se movió ⇒ 837 artefactos
intactos), y el plan del §8: la unidad 2 queda cerrada, la 3 pasa a ser construir, y el PGO se
separa como 3.a porque tiene sus propios dos muros (sin X11 para el profileserver, profdata no
determinista).
2026-09-05 03:14:02 +00:00
Sergio cb3ecd5a00 firefox pasa a clang+lld con LTO: la puerta de las optimizaciones costaba apk add lld, no una receta de LLVM
El SDD 26 estimó que habilitar PGO/LTO exigía una receta `llvm-toolchain` (clang+lld+libc++ desde
fuente). ERA CARO DE MÁS. Al mirar el lab en vez de suponerlo:

- `.dev-fs/alpine` YA TRAE clang22 + llvm22 22.1.8 — la MISMA major que usa el APKBUILD de Alpine
  para este mismo Firefox (`_llvmver=22`).
- `Compiler::Clang` YA EXISTE en hammer, cableado de punta a punta: `parse_compiler` lo acepta y
  `hammer-build/src/lib.rs` pone CC=clang, CXX=clang++ y AR=llvm-ar. NINGUNA receta lo usaba.
- Lo único que faltaba era `ld.lld`. `apk add lld` ⇒ lld22 22.1.8, dos paquetes, cero upgrades.

CON ESO CAEN LOS TRES MUROS QUE OBLIGABAN A gcc, sin perder lo que gcc daba: el sondeo de linker se
satisface con `--enable-linker=lld`, el `ar` lo pone hammer solo, y el `NEEDED` de la stdlib de C++
existe porque clang++ de Alpine usa la libstdc++ COMPARTIDA — la prueba no es teórica, Alpine
construye este Firefox con clang22 y sin libcxx en sus makedepends.

LA HUELLA DEL LAB NO SE MOVIÓ, Y SE MIDIÓ ANTES DE TOCAR NADA. `lld` no casa ningún prefijo de
TOOLCHAIN_PREFIXES (hammer-core/src/lab.rs), así que los 43 paquetes que entran en `hash_inputs`
salieron idénticos ⇒ los 837 artefactos sellados quedan intactos. Eso es lo que hace barato el
cambio HOY, y a la vez es un agujero escrito en los dos sitios: la versión de lld no es parte de la
identidad del artefacto, y sólo expone a las recetas `compiler="clang"`, que hoy es una. Meter
"lld" en la lista de prefijos es lo correcto y cuesta re-hashear el corpus entero: próxima campaña.

En el mozconfig entran, además de lld: `--enable-lto=cross`, `--enable-packed-relative-relocs` y
`--with-unsigned-addon-scopes=app,system`. El último no es cosmético: sin él un Firefox de release
rechaza las extensiones que la distro deja en distribution/extensions/, así que la capacidad de atuq
de shipear su propio `sct` se decide ACÁ, en la base, y no en el overlay del derivado.

PGO no entra en esta pasada y el porqué queda escrito en la receta: el perfil se junta corriendo el
navegador (Alpine y Arch usan xvfb-run; nosotros no tenemos X11 ⇒ sway headless) y el profdata NO es
determinista, así que tiene que sellarse como artefacto propio y consumirse por hash.

ThinLTO se capa con la misma cuenta que -j y por la misma razón que ella no es un literal: un número
fijo ataría el ArtifactHash a la RAM de quien escribió la receta.

Nuevo ArtifactHash: b3:6f2a3b2f6db4452ed0d2d3f4e2ff7cd6562a86878d4360653859720be1c3d94d
(el firefox 154.0 sellado con gcc queda SUPERADO, no perdido).
2026-09-05 03:13:38 +00:00
Sergio 42404bc692 SDD 26 §3.bis: el diff contra Alpine dice que no es sólo PGO, y una de las diferencias es de seguridad
Pregunta del usuario: «¿firefox está sin pgo-lto-bolt? ¿eso es una desventaja frente a las otras
distros?». Sí, y para medirlo se trajeron el APKBUILD + mozconfig de community/firefox de aports y
el PKGBUILD de Arch, y se compararon línea a línea con el mozconfig de recipes/firefox.toml.

La comparación importa porque ALPINE ES NUESTRO PROPIO UPSTREAM: de ahí salen los once parches de
musl. No es una distro lejana sacándonos ventaja, es la que ya construye este mismo código con
`--enable-lto=cross` + `--enable-profile-use=cross` + jarlog. Arch hace lo mismo («Do 3-tier PGO»).

BOLT NO ES LA DESVENTAJA: cero menciones en los dos ficheros. Lo que nos separa es PGO+LTO.

Y el diff completo da más que velocidad:
- RLBOX APAGADO (`--without-wasm-sandboxed-libraries`) es una desventaja de SEGURIDAD y pesa más que
  el PGO: es la jaula wasm de graphite/ogg/expat/woff2, o sea el código que come entrada no
  confiable. Encenderlo pide wasi-sdk + wasi-compiler-rt en el corpus: receta, no flag.
- `--with-unsigned-addon-scopes=app,system` nos falta y ATUQ LO NECESITA para shipear sus propias
  extensiones ⇒ una capacidad de atuq que NO se resuelve en el overlay: exige tocar la base.
- Falta RELR; el linker es GNU ld y no lld.
- `--disable-jemalloc` NO es desviación nuestra: Alpine también lo pasa. Un fantasma menos que
  perseguir.
- Bundlear en vez de `--with-system-*` es diferencia a propósito: el corpus ES la fuente.

Corolario que ordena el plan: TODO ESO ENTRA EN UN SOLO REBUILD. Cada uno son cuatro horas y
re-sella la cola Gecko entera, así que la unidad 3 pasa a ser una pasada única y RLBox se separa
como 3.b.

Y dos muros del PGO que las distros no tienen, porque no persiguen lo que nosotros perseguimos:
- las dos corren el profileserver bajo `xvfb-run`, y NO TENEMOS X11 en el corpus (Wayland-only) ⇒ va
  bajo sway headless, que ya existe del frente wlr.
- el profdata NO ES DETERMINISTA (contadores dependientes del timing) ⇒ se genera una vez, se sella
  como artefacto propio y se consume por hash, o firefox deja de reproducir.

Bonus para el §2: el jarlog del PGO ordena el omni.ja para el arranque. Re-empacarlo a lo bruto tira
esa optimización a la basura sin que nadie lo note.
2026-09-05 03:03:23 +00:00
Sergio 1fcad3f6de SDD 26: atuq, el envoltorio Gecko — derivado, no fork; y la puerta es clang
La pregunta era «nuestro propio envoltorio en vez de zen». La respuesta tiene tres hallazgos que
cambian el precio, y el documento los pone antes que la lista de features.

1. NO HAY ENVOLTORIO FUERA DEL CHROME. Gecko no tiene API de embebido en escritorio desde que murió
   XULRunner (GeckoView es Android) ⇒ una carcasa Llimphi con el motor adentro no es posible. El
   envoltorio es chrome-level o no es. Que es exactamente lo que Zen es.

2. ARTEFACTO DERIVADO, NO FORK DE FUENTE. Si atuq parchea el árbol, cada iteración de UI cuesta
   cuatro horas de Gecko y el diseño de chrome es iterativo por naturaleza. Como no distribuimos un
   binario sino que sellamos artefactos, atuq puede depender de `firefox` e inyectar marca, prefs,
   policies.json y omni.ja encima: itera en segundos, no mueve el ArtifactHash del corpus, y hereda
   las CVE gratis — que es lo que hundió a los forks de Firefox. Con sus dos gotchas escritos: el
   omni.ja es un zip y hay que re-empacarlo determinista, y sin invalidar el startup cache el
   overlay PARECE no agarrar.

3. LAS OPTIMIZACIONES NO SON UN FLAG, SON UNA TOOLCHAIN. PGO/LTO/BOLT son cadena de clang en Gecko,
   y el corpus no tiene clang usable como compilador: clang18.toml compila SÓLO libclang.so (para
   bindgen) y llvm18 se selló con PROJECTS="". La unidad real es una receta llvm-toolchain con
   clang+lld+libc++ musl — la misma pieza que resuelve el muro 3 de firefox.toml. Es la mayor
   palanca del frente, y por eso encabeza el plan.

Lo que NO se promete va escrito ANTES de la lista de features, con el criterio del modelo de
adversario de qullqa: atuq no es Tor Browser y no va a tener «modo Tor» — es un binario único en el
mundo (musl, wayland-only, branding propio), así que salir por Tor desde acá identifica MÁS, no
menos. Se ofrece proxy por contenedor, que es separación de tráfico y no anonimato.

Los diferenciadores van con una columna «quién más lo tiene» para no contarnos un cuento: sct
(transparencia de scripts, BLAKE3 + testigo) no lo tiene nadie y ya está escrito en tawasuyu;
torrent SÍ lo tiene Vivaldi, y lo nuestro sólo vale por dónde cae lo bajado (el CAS). Todo lo que
no es CSS pasa por UNA costura: un host de native messaging en Rust.

Y atuq no abandona a puriy: el host, el testigo y el archivo con RAG son agnósticos del motor, así
que cuando puriy madure se enchufan del mismo lado. atuq es su andamio, no su desvío.
2026-09-05 02:54:23 +00:00
Sergio 1a3b62b1f6 estado: cosecha granja 2026-09-05T02:33:35Z — avance del árbol KDE 2026-09-05 02:33:35 +00:00
Sergio aa200a4e7e vigía de parches: probar que agarran ANTES de las cuatro horas de build
`hammer build` aplica los parches DESPUÉS de materializar las dependencias, así que en una receta
hoja de la plataforma Gecko el `patch` que no agarra se descubre detrás de horas de compilar OTRA
COSA: waterfox estrena su primer build reconstruyendo nodejs entero (4414 objetivos de V8, a -j2
porque la propia receta capa por RAM). La receta de waterfox dice, con todas las letras, que si
patch falla «falla TEMPRANO, antes de compilar nada». Con el orden real de hammer eso no era
cierto. Este vigía es lo que lo vuelve cierto: saca de los propios parches los ficheros que tocan
(21 en waterfox), los trae por sparse-checkout blob:none, y aplica los once acumulativos y en
orden como hace fetch.rs. Un minuto en vez de cuatro horas.

LO QUE MIDE NO ES SÍ/NO, SON TRES ESTADOS. `patch` también responde «sí, adivinando»: cuando el
contexto no casa aplica igual con FUZZ, y con el `--silent` de fetch.rs eso sale por exit 0 sin que
nadie se entere. Un parche con fuzz puede haber editado el sitio correcto o cualquier otro, y el
exit code no distingue. Por eso `ok` / `FUZZ` / `FALLA`, y el del medio existe para no perderse.
El desplazamiento no se marca: sólo dice que el fichero creció por arriba.

VEREDICTO SOBRE LA APUESTA DE WATERFOX (parches de 154 sobre base 153.1.0): SE SOSTIENE. Los once
entran; los dos que entran con fuzz —time64 y fix-rust-target— se verificaron a mano y los dos
editan el sitio correcto. El contexto que no casa es cosmético: comillas simples vs dobles de un
reformateo con black, y un brazo `cfg` de más.

Y UN HALLAZGO DE PASO: `firefox-patches/time64.patch` tiene la cabecera MENTIROSA. Su diffstat
anuncia tres ficheros y el cuerpo entrega dos — falta el hunk de `wgpu-hal/src/vulkan/adapter.rs`.
Como firefox 154 sella igual, nadie lo había notado. Por eso los ficheros se leen del cuerpo y
nunca del diffstat.

El vigía nació mintiendo en la dirección cara y por eso se probó contra recetas selladas antes de
commitear: suponía el prefijo `a/`+`b/` de git y marcaba FALLA sobre gawk, que sella perfecto — su
parche es un `diff -upr` a secas con caminos `gawk-5.1.0.orig/`. Se descarta el primer componente
se llame como se llame, que es lo que significa el -p1 con el que se aplican.

Flags en inglés (--all, --git-only, --keep, --strict) por la regla 4 del repo.
2026-09-05 02:19:04 +00:00
Sergio ad01dea4c2 estado: cosecha granja 2026-09-05T02:02:02Z — avance del árbol KDE 2026-09-05 02:02:02 +00:00
Sergio fb917cbb79 estado: cosecha granja 2026-09-05T01:31:49Z — avance del árbol KDE 2026-09-05 01:31:49 +00:00
Sergio 067631fa52 estado: cosecha granja 2026-09-05T01:02:18Z — avance del árbol KDE 2026-09-05 01:02:18 +00:00