**curl ya valida**: `ca-certificates` re-sellado con **119 hash-links en el capath**, generados en el
build, y la caja baja por HTTPS sin apaño (200). Tardó tres intentos y los dos primeros los cazó la
guarda de la propia receta: `c_rehash` no estaba en el PATH (lo instala esa misma receta en
`/out/usr/bin`) y los certificados no están sueltos en `usr/share/ca-certificates/` sino en
`mozilla/`, así que sólo veía el bundle y lo saltaba —correctamente— con «does not contain exactly
one certificate». Sin la guarda habría sellado un capath vacío las tres veces.
**fastfetch 2.68.1 corre en la caja** (pedido del usuario) y al correrlo destapó que
**`/etc/os-release` no existía**: la distro era anónima para cualquier programa que no fuera suyo.
Ahora `OS: takana x86_64`. Va como receta propia (`source.dir`) y no como constante de
`takana-bootstrap`: meterlo ahí re-sellaría el product-rootfs —el baseline del selfhost— por cinco
líneas. Sin VERSION_ID ni fecha: un sello con la fecha del build cambiaría el hash cada día sin
motivo, y con él la clausura de toda imagen que lo lleve.
🧨 Y el hallazgo estructural: `upgrade apply` falló dos veces con **No space left on device teniendo
3 G libres en la raíz**. No era `/` ni los inodos (9%): el estado de generaciones vive en
**`/var/lib/hammer`, que es sda3 y mide 487 MB** — la partición «estado» del layout. Cada generación
guarda una copia del árbol aplicado (272 MB el nuestro), así que dos no caben. Movido a `/work` por
symlink, igual que qorpa, y la generación 6 entró. El layout de la imagen necesita revisión: 512 MB
no alcanzan para un mecanismo que guarda un árbol por generación, y el síntoma apunta al sitio
equivocado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
**minga con su daemon escuchando.** Decisión del usuario: minga es la ventana oficial al mundo y git
queda como espejo de compatibilidad, así que el daemon no es opcional — es el punto de entrada. El
binario ya está sellado (`b3:121cf4a8…`, 22 M, static-pie) y ahora trae su `[[user]]` (uid 970, home
`/work/minga`: en la raíz no cabe y `/work` sobrevive a un re-`dd`) y su `[[service]]`:
`listen /ip4/0.0.0.0/tcp/4001` — la convención libp2p, libre en la caja (ocupados: 22022 admin,
2345 git-SSH, 3002 gitea en loopback, 80/443 caddy). Declarar ambos **no movió el ArtifactHash**.
⚠⚠ **La identidad no se sella ni se inventa.** El keypair lo genera el usuario con `minga init`;
tawasuyu avisó de que el suyo era provisorio. Si la card hiciera un `init` automático, la caja se
inventaría su identidad soberana en el primer arranque y todo lo que se firme después colgaría de
ella. La guarda comprueba que el repo exista y sale 78: un peer sin identidad decidida es peor que
un peer ausente.
**fastfetch** (2.68.1) entra al corpus y a `perfil.cli` — lo heredan servidor y los escritorios.
Receta CMake con `-DCMAKE_LINK_DEPENDS_USE_LINKER=OFF` (sin eso el lld de zig segfaultea y el error
no nombra a zig) y con la detección opcional APAGADA explícitamente: con `link = "static"` no hay
`dlopen`, y una detección que entra «porque la librería estaba en el lab ese día» produce artefactos
distintos bajo el mismo nombre.
Y `ca-certificates`: el rehash se llamaba por nombre y **`c_rehash` lo instala esta misma receta en
`/out/usr/bin`**, no está en el PATH del sandbox. Se invoca por su ruta. La guarda que añadí ayer
hizo su trabajo: el build falló ruidosamente en vez de sellar otra vez un capath sin índice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Al traer la primera imagen ajena, la caja falló con «curl failed to verify the legitimacy of the
server». El certificado del servidor estaba bien; lo que falta es de este lado:
* CApath: /etc/ssl/certs
* SSL certificate ... unable to get local issuer certificate (20)
$ curl --cacert /etc/ssl/certs/ca-certificates.crt ... → 200
El curl del corpus se compila con `--with-ca-path=/etc/ssl/certs` y SIN CAfile, y ese directorio
tiene un solo fichero: el bundle. Un capath necesita hash-links (`c_rehash`) — el ÍNDICE — y no los
hay. Los certificados están; el índice no.
Por qué faltan: la receta deja el hook `/etc/ca-certificates/update.d/certhash`, que en Alpine
ejecuta `apk` al instalar. **En takana no lo ejecuta nadie**: la distro proyecta artefactos, no corre
post-install. Todo presente, y el paso que lo conecta no existe.
Alcance: no es sólo `qorpa pull`. Es `install --repo https://…`, el mirror y cualquier curl/wget de
una caja instalada. No falla al construir ni al instalar: falla la primera vez que la máquina intenta
bajar algo. No se había visto porque el hub usa el curl de Artix.
Arreglado en la receta: los hash-links se generan EN EL BUILD (donde c_rehash y perl existen) y
viajan en el artefacto, con una guarda que falla ruidosamente si el rehash no produce ninguno.
Re-sella `ca-certificates`, raíz de `perfil.base`. Apaño mientras tanto: `CURL_CA_BUNDLE=…` (200).
Y el estado de qorpa en la caja: preflight de «BLOQUEA 1 · LIMITA 4» a «LIMITA 1» — qorpa movido a
/work (la raíz tenía 3 G), subuid provisionado y `setcap` aplicado a newuidmap/newgidmap SIN tocar el
artefacto del store (inodes distintos: en una caja instalada esos binarios son copias). El pull baja
y verifica el sha256, y muere al desempacar: el rootfs de Arch es `.tar.zst` y el `tar` de la imagen
es el de busybox. `qorpa pull` necesita `zstd -dc | tar -x` para `.zst`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Antes de tocar `sergio.gioser.net` apareció lo urgente: CINCO procesos vivos cuyo binario ya no
existe en disco (`/proc/<pid>/exe` → «(deleted)»). Sólo vivían como inode huérfano: si el proceso
muere o la máquina se reinicia, se pierden. Es el «paso que caduca» del SDD 29, caducando de verdad.
tejido · sandokan-mcp · pacha-secretos · puerta-f6e393ffbef3999e · shuma-gateway (240 M)
Recuperados leyendo `/proc/<pid>/exe` con el proceso vivo, y ya están FUERA de gioser: en
`/work/rescate-binarios/` de la caja y en el Storage Box. Cinco de los trece binarios que nadie
provee dejaron de depender de que nadie reinicie nada.
Y `sergio.gioser.net` son tres piezas, de las que sólo una se muda hoy:
· frontend: 117 M de estático ⇒ copiado a `/work/www/sergioh` ✅
· `/shuma/*`: `shuma-gateway` en :7378, ELF dinámico y con el binario borrado ⇒ hay que construirlo
desde tawasuyu (es Rust del monorepo, factible)
· `api.sergio`: uvicorn con un venv de **405 M** — fastapi + pydantic + google-generativeai +
**langchain**, con extensiones `cpython-314-x86_64-linux-GNU.so` ⇒ **glibc**. Reempaquetar ese
árbol de PyPI como recetas no es realista y en musl no corre. Es el caso canónico del ADR 0015
(qorpa), que sigue PROPUESTO.
Por eso el DNS de `sergio` NO se movió: mover el nombre sin backend deja el sitio servido y la
consola rota. El frontend queda copiado y esperando.
Esto es lo que decide la fecha de borrado de gioser: no es «mudar dominios», es que un servicio vivo
no tiene hoy camino a takana. Tres salidas, y ninguna es técnica: qorpa · que viva en otra máquina
(summa ya lo hace) · o que muera.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Antes: dos reconstrucciones daban `libKF6SyntaxHighlighting.so` distintos —
8.800.310 bytes diferentes desde el offset 41, y hasta el tamaño cambiaba
(10.345.888 contra 10.345.840). Ahora: REPRODUCE.
Hash: 4c1638a0… → 3db73c19… (arrastra 4 dependientes directos, 5 transitivos).
── Dónde NO estaba, que también cuenta ────────────────────────────────────────────────
No era el indexer: `katehighlightingindexer` arma el índice con `QVariantMap`, que es
QMap y va ordenado. No eran los generadores de Perl: ninguno de los cuatro itera un
hash. Mirarlo antes ahorró parchear lo que no era.
── Dónde estaba: `data/generators/generate_jinja.py`, con tres dependencias del azar ──
1. `to_do.pop()` sobre un `set` saca un elemento ARBITRARIO. En `--dry-run` el orden de
los `print(out_file)` es lo que CMake recoge en `out_xmls`, y eso acaba siendo **el
orden de las entradas del `.qrc`** ⇒ el recurso compilado cambiaba de disposición
entera. Se toma el menor: mismo conjunto, orden fijo.
2. `version = str(round(time.time()))` hornea la HORA DEL BUILD en el XML generado. Se
honra `SOURCE_DATE_EPOCH`, que es la convención de reproducible-builds y que el
sandbox de takana ya fija (=1).
3. `os.listdir()` sin ordenar. Sólo importa si dos ficheros declaran el mismo lenguaje,
pero quitar la dependencia del readdir no cuesta nada.
── Medido en los dos sentidos ANTES de tocar la receta ────────────────────────────────
Corriendo el generador a mano, sin builds de por medio:
· antes — tres PYTHONHASHSEED distintos ⇒ TRES md5 distintos, y el orden salta a la
vista: `jinja-json, jinja-yaml, jinja-toml…` contra
`jinja-qml, jinja-dockerfile, jinja-typescript…`
· después — las mismas tres semillas ⇒ el MISMO md5
· generando de verdad con semillas Y momentos distintos ⇒ los 35 XML IDÉNTICOS,
con `version="1"`
· control negativo — sin `SOURCE_DATE_EPOCH` sigue poniendo la hora actual
(comprobado: coincidía con `date +%s` al segundo) ⇒ fuera del sandbox no cambia nada
Primer aviso de esta medición: mi primera comparación dio «idéntico con las tres
semillas» y era MENTIRA — el script salía con «Destination folder does not exist» y yo
comparaba md5 de un mensaje de error. Comparar salidas sin mirar que la herramienta
hiciera algo es inventarse un control.
Con -vga none la captura ya no tiene punto ciego, asi que un ✗ significa
«la ventana no esta». Clasificado el ultimo cuadro de las 10: 2 con la
pagina pintada, 1 con la ventana en blanco y 7 con SOLO el escritorio.
Las dos salidas explicaban los ✗ inflados de las series VGA=1, no el
fenomeno.
El MOZ_LOG separa los dos regimenes: las que tienen ventana escriben
952-1505 lineas hasta el apagado; las 7 sin ventana se cortan a los ~40s
y quedan 6 minutos de silencio, sin crash (WebRender arranca). Quedan
como hipotesis NO medida los frame callbacks que el compositor no manda.
Ademas: tasa-primera-pintura.sh nombra el log por VGA (la serie nueva
habia sobrescrito los crudos de la anterior).
Medido dominio por dominio (DNS por DoH **y** HTTP contra la IP del origen) antes de tocar nada:
5 fósiles sin DNS (aura, api.aura, sigma, kosmofono, api.kosmofono) · 3 que ya viven en OTRA máquina
(summa, dev.summa, api.dev.summa → 154.197.1.2, así que sus bloques en gioser eran código muerto) ·
2 en 502 permanente (mail.sigma con :9000 caído, api.gioser.net con :8000 caído) · 5 mudados hoy ·
y **2 vivos de verdad**: `sergio.gioser.net` y `api.sergio.gioser.net`.
Dato que ordena: **ninguna de las raíces estáticas de los fósiles existe en disco**
(`/var/www/{aura_frontend,sigma,kosmofono,summa,summa-dev}`). El Caddyfile servía directorios
ausentes — la configuración sobrevivió a sus datos. De los backends sólo `:8770` escucha, y su
dominio ya apunta a otra parte.
Los 10 bloques muertos fuera (respaldo previo, `validate`, `reload`), con control: `sergio` y
`api.sergio` siguen en 200, y los mudados responden desde la caja.
⚠ El único fósil CON datos es `terapeuta.ec`: 279 M en `/var/www/terapeuta`, sin DNS y sin vhost. No
se borra acá; se anota para la decisión de borrado de la máquina.
Lo que queda por mudar es un frente acotado: un backend Python en :7378 y una API en :8771.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
ksvg, qqc2-desktop-style, kirigami-addons y libplasma, ya reconstruidos contra el
kirigami arreglado. REPRODUCEN 4 · DERIVA 0 · NO-DETERMINISMO 0.
No es adorno: los cuatro llevan QML, que es donde vivía la carrera del AOT. Que el
árbol reconstruido reproduzca es lo que cierra el arreglo — sellar de nuevo no
demuestra nada por sí solo.
El usuario avisó de que esa máquina está con arje desde hace meses. Tiene razón, y las dos cosas son
ciertas: PID 1 **es** `arje-zero`, la card `openrc-gitea` **no** llama a `rc-service` (ejecuta el
binario directo — el prefijo es herencia del nombre que le puso `arje-absorb` al traducir), y sin
embargo **`/run/openrc/started/` existe con servicios dentro** (NetworkManager, dbus, dhcpcd,
localmount…) y `rc-update show default` listaba gitea. OpenRC quedó como residuo ACTIVO, capaz de
arrancar por su cuenta: eso fue lo que revivió al gitea con ppid ≠ 1 después de que arje lo parara.
Regla para el resto de la mudanza: antes de dar un servicio por apagado, preguntar a los DOS —
`arjectl list-units` y `rc-update show default`. Un nombre que dice `openrc-` y no es OpenRC, al
lado de un OpenRC real que nadie esperaba que siguiera operando.
Y mudados `takana.gioser.net` y `hifas.gioser.net`: estáticos puros, 52 K, vhost en la caja, DNS de
CNAME a A, fuera del origen. 200 con TLS válido desde 2.29.29.217; `sergio.gioser.net` sigue en 200
y gioser baja de 16 a 14 vhosts.
Se repitió lo del ACME: el primer certificado se pide antes de que propague el DNS y falla contra el
origen. Conviene mover el DNS y RECIÉN ENTONCES añadir el vhost, o asumir un `restart` de más.
Los 14 que quedan tienen backend propio o son fósiles: no se mudan copiando un directorio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
El arreglo del no-determinismo de kirigami movió su ArtifactHash (41eea38e… →
a72b1626…) y con él el de todo lo que cuelga: 37 dependientes directos, 39
transitivos (entran `frameworkintegration` y `plasma5support` por vía indirecta).
Reconstruidas las 39 en orden topológico y con UN solo `flock -o` para toda la
tanda. Comprobado aparte del log: para las 39 existe `store/<hash-de-hoy>-<nombre>`.
Las gordas: kwin 3027s, plasma-workspace 1958s, okular 810s, kirigami-addons 816s.
⚠ A mitad de tanda, `kirigami-addons` y `kdf` murieron con «No space left on
device» — y es otra vez la trampa de siempre: NO eran recetas rotas. El disco de
`/mnt/vvv` estaba al 100%. Las dos reconstruyen bien con sitio libre.
Dos cosas se arreglaron a raíz de eso:
· `poda-fuentes.sh` con su suelo de 24 h liberaba CERO, porque los 30 árboles
eran todos de hoy — la lección de `cache-ci-no-envejece` otra vez: un guardián
calibrado a una escala que el dato nunca alcanza no protege. Con `--horas 1`
y luego `--horas 0` se recuperaron 14 G.
· la tanda ahora poda el árbol de CADA receta al terminarla (sólo el suyo: las
deps son compartidas, borrarlas en vuelo es el ADR 0012). Con eso
`work/sources` se mantuvo en 54-71 M durante el resto de la cascada en vez de
crecer sin freno.
El `never: 1` que aparece en el grafo KDE es `minga`, una receta nueva de otro
agente, sin perfil y ajena a esta cascada.
Comparados los 845 refs de los dos lados, repo por repo. Dos diferencias: una esperada
(`sergio/takana` main, el nuevo por delante con los commits del cutover — fast-forward verificado)
y una que NO: `tawasuyu/tawasuyu` tenía en el viejo `b69008eb` (freebsd) y `02a9535f` (shuma,
**19:48**), quince minutos DESPUÉS del snapshot final.
Por qué: el gitea viejo había resucitado por OpenRC y un cliente con el DNS cacheado (el CNAME
anterior tenía TTL 600) lo empujó a gioser aunque el autoritativo ya dijera la caja. Hacían falta
las dos condiciones a la vez.
Recuperados con un push del ref exacto desde el clon local; la UI del nuevo ya muestra `02a9535f`.
Segunda pasada: 845/845 y una sola diferencia, la esperada. En metadatos, `action` tenía tres filas
posteriores al snapshot (los mismos dos repos) y CERO issues/comentarios/releases.
El orden para el resto de la mudanza: bajar el TTL ANTES del corte · apagar el origen de verdad (los
dos supervisores) ANTES de tocar el DNS · comparar refs después, siempre · y dejar el origen apagado
un rato antes de borrarlo, precisamente para poder comparar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
⚠ EL GITEA VIEJO HABÍA VUELTO A ARRANCAR. Media hora después del corte, gioser servía otra vez en
:3002. `arjectl list-units` ya no lo mostraba —el stop de arje seguía en pie— y el proceso tenía
ppid ≠ 1: lo levantó **OpenRC**, que lo tenía en el runlevel `default`. Dos supervisores para el
mismo servicio, y parar uno no para el otro. Antes del corte pasaba lo contrario: `rc-service stop`
decía «already stopped» con el proceso vivo, porque quien lo tenía era arje. La pregunta no es
«¿está parado?» sino «¿QUIÉN lo tiene?», y hay que responderla dos veces.
Arreglado con `rc-update del gitea default` + `rc-service gitea stop`.
El origen deja de servir git: los tres bloques salen del Caddyfile de gioser (con respaldo,
`validate` y `reload`) y quedan 16; control inmediato de que no rompí lo demás —`sergio.gioser.net`
y `hifas.gioser.net` siguen en 200—. `gitea.gioser.net` pasa a A → la caja.
El `:22` cerrado sin perder el acceso: administración en **22022**, git por SSH en **2345**, y el 22
en `Connection refused`. La secuencia es la única segura: añadir el puerto nuevo → reiniciar →
ENTRAR por él → sólo entonces quitar el 22. Verificado en ese orden, y el clone por 2345 sigue.
🧨 Y lo que apareció al mirar: **los datos del gitea nunca estuvieron respaldados**.
`respaldo-storagebox.sh` cubre el STORE de artefactos, no `/var/lib/gitea` — los 44 repos vivían en
una sola copia. La mudanza no lo empeoró, pero lo vuelve urgente porque gioser se borra. Hecho hoy
desde la caja: snapshot de la DB con el servicio vivo + rsync a `u647150:gitea/` (1,7 G).
⚠ Al Storage Box se entra por el puerto **23**: el 22 da SFTP restringido y contesta «Permission
denied (publickey,password)», que se lee como «no tengo la clave» teniendo la clave perfecta.
Pendiente: que ese respaldo sea periódico — un renglón en el cron de la caja.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
`git.gioser.net` y `git.tawasuyu.net` responden **200 con TLS válido desde 2.29.29.217**, `git clone`
funciona por HTTPS **y** por SSH:2345, y `gitea` + `caddy` corren supervisados por `arje-zero`. El
gitea de gioser está parado. Es el primer servicio real que deja la máquina que se va a borrar.
Mudanza INCREMENTAL: los DNS de gioser son casi todos `CNAME → www`, así que mover `www` habría
mudado quince dominios cuyos backends siguen allá. Se convirtió sólo `git` (las dos zonas) de CNAME
a A propio con TTL 60 — reversible en un minuto.
Cinco cosas que sólo se aprenden haciéndolo:
· **Parar el origen no es `rc-service gitea stop`**: dice «already stopped» con el proceso vivo, y
matarlo no alcanza — lo revive arje-zero, que lo tiene como card `openrc-gitea` con Restart y
**9001 reinicios** en el contador. Se paró con `arjectl stop openrc-gitea` (el arjectl que
construimos hoy, hablando con el arje de gioser), y para eso hubo que extraer su card del genesis
y escribirla en `cards.d` — que es justo el hueco del §6.12.
· **El token de `hcloud` también gestiona el DNS** (Cloud API unificada, `/v1/zones`); la API vieja
`dns.hetzner.com/api/v1` redirige a la consola web. Y un `PUT` sobre el rrset no puede cambiar el
TIPO: hay que DELETE del CNAME y POST del A.
· **ACME falló primero contra gioser** (502) porque el challenge salió antes de que propagara el
DNS. Con el DNS al día, `arjectl restart caddy` → certificate obtained successfully.
· **La identidad SSH se muda con el servicio**: `REMOTE HOST IDENTIFICATION HAS CHANGED` hasta que
se copiaron las claves de host de gioser a la caja. Así los clones existentes no notan nada; el
precio es limpiar el known_hosts propio del `:22` de administración.
· El `sshd` del producto escucha sólo en `:22` ⇒ el git por SSH necesitó `Port 2345` y que el
usuario `gitea` tenga shell real (su authorized_keys fuerza `command="gitea serv …"`).
`caddy` gana su `[[service]]` (con guarda del Caddyfile, HOME propio para los certificados de ACME
—o cada reinicio pediría certificados nuevos y se comería el límite de emisión— y la nota de por qué
corre como root) y el perfil lo arranca.
Consecuencia para la granja: las 23 recetas que clonan por `https://git.tawasuyu.net/…` **ya apuntan
a la caja**. Comprobado: el worker resuelve 2.29.29.217 y `git ls-remote` responde.
Revertir, si hiciera falta: `arjectl start openrc-gitea` en gioser y los dos `git` de vuelta a CNAME.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Tres cosas, y las tres quitan vueltas.
1. NO HACE FALTA NINGUNA CREDENCIAL EN EL WORKER, Y TAMPOCO UN USUARIO NUEVO
`tawasuyu/tawasuyu` y `sergio/takana` son repos PÚBLICOS en el gitea. Las 23 recetas que apuntaban a
`gitea@git.tawasuyu.net` (SSH, que exige la clave que es el SSH de todo) pasan a
`https://git.tawasuyu.net/…`. La URL es locator y NO entra en `hash_inputs` (ADR 0013): hecho con
control antes/después, **ningún ArtifactHash se movió**. Medido en el worker: `git clone --mirror
--filter=blob:none` + `git archive` extrae el árbol (155 M) **sin una sola credencial**.
Un usuario propio en gitea sólo haría falta para un repo PRIVADO; hoy ninguna receta usa uno. Si
mañana hace falta, es una cuenta de máquina con acceso al repo que toque — nunca la clave personal.
De paso, tres recetas decían «HUB-ONLY: el worker secretless recibe Connection refused». Ya no es
cierto y el comentario decía lo contrario de lo que pasa: corregido en las tres.
2. LA COPIA FUERA LA DA LA MUDANZA, NO GITHUB (decisión del usuario)
El «paso 1: espejar los 26 repos» deja de ser el paso 1. En cuanto el gitea vive en la caja nueva,
esos repos dejan de existir sólo en la máquina que se borra — que es lo que el paso pedía. El
objetivo es dejar de pagar dos máquinas, no sumar una dependencia. `espejar-repos.sh` queda como
herramienta disponible.
3. EL §9 REESCRITO: qué está hecho, qué falta y en qué orden
Hecho y cerrado: la caja arranca takana puro · store/grafos/granja/respaldo · repo firmado · la
imagen del perfil servidor con gitea sirviendo 200 supervisado por arje · `arjectl` · y el ensayo con
los datos REALES de gioser corriendo sobre takana.
Falta, en orden: actualizar la caja a la imagen nueva (`upgrade`, no `dd`) → mudar los datos del
gitea (`.backup` + rsync, con el origen parado en el corte) → caddy con los 6 dominios vivos → DNS →
verificar desde fuera con un clone real → el resto de servicios del censo → borrar gioser con las 8
puertas en verde.
Bloqueantes con su tamaño: `git` sellado sin `remote-http` (afecta a clonar DESDE una caja takana,
no al gitea que sirve; re-sella una raíz de `base`) y la puerta 5, que no bloquea la mudanza.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Dos cosas antes de la medición: el MOZ_LOG de las que pintan y las que no termina IDÉNTICO (las dos
commitean WaylandBufferSHM de 1280x696), y el cruce de las 15 corridas está confundido — las 13 de
«perfil del usuario» salieron todas con VGA=1.
`screendump` de QMP fotografía UN dispositivo. Con VGA=1 hay dos, y hasta ahora se miraba uno. Con
`id=` en cada uno y capturando los dos en cada toma, en la misma corrida y el mismo instante:
+312s ██ PRIMERA PINTURA — 704456 px EN vga0
gpu0 = 962675 px de fondo + panel y dock (el escritorio, SIN navegador)
vga0 = 703766 px magenta (la ventana, con la página)
⇒ cosmic-comp maneja las DOS salidas y la ventana cae en una u otra. «Pinta 3 de 13» era cuántas
veces cayó en la pantalla que yo fotografiaba: una tasa de mi cámara, no del producto. El §6.10.ter
tiene la misma explicación (aquel arranque también traía `drm: card0 card1`).
Queda en pie, ya sin confundido: el navegador arranca, mapea y commitea SIEMPRE; cuando la ventana
está en la pantalla que se mira, se ve entre +184 y +312 s; el perfil no era la causa y cosmic-comp
nesteado tampoco.
⚠ Tercera vez en el día con la misma forma: un ✗ de una captura de UNA pantalla no dice «la ventana
no está», dice «no está en ESA» — igual que `mapped 1` no probaba que se viera. El estado anota ahora
en qué pantalla apareció y cuántas se fotografiaron, y el vigía NO cuenta las ciegas: las nombra.
Diez corridas idénticas con `scripts/cosmic/tasa-primera-pintura.sh` (--as-user, VGA=1, ventana 480 s,
captura cada 30 s), más las cinco anteriores, todas anotadas en docs/state/primera-pintura.json:
perfil del usuario 3 de 13 pintaron 184, 199, 204 s
perfil nuevo en tmpfs 2 de 2 197, 197 s
Dos cosas que la tasa dice y una anécdota no podía: cuando pinta, pinta SIEMPRE en la misma ventana
(184…204 s) y nunca a los 300, 400 ni 900 ⇒ no es una cola larga, son DOS REGÍMENES —o sale a los
~200 s o no sale—; y con el perfil del usuario falla ~3 de cada 4, mientras que con perfil nuevo en
tmpfs no falló (2 de 2, pocas corridas para afirmar que nunca, suficientes para saber dónde mirar:
qué hace el primer arranque del perfil que el perfil ya hecho no hace).
⚠ Condición de la medida, parte del número: las diez salieron con el anfitrión a load ~10 (tanda de
KDE de otro agente + una VM de otra sesión, en 4 cores). El número es un PISO, no una constante.
⚠ Y una trampa del arnés medida en las corridas 8–10: con esa carga el guest tarda más de 420 s en
llegar al shell y los `esperar` vencen. No las invalida —se verificó una por una que el navegador se
lanzó y dejó su MOZ_LOG de 1200–5000 líneas— pero una corrida abortada de verdad se ve casi igual:
por eso el guion cuenta las abortadas APARTE en vez de sumarlas a los fallos.
Sin tocar gioser (todo lectura) y en la VM desechable. Dentro de la VM: `<title>GioSer Gitea: Git
with a cup of tea</title>`, 200, `/explore/repos` listando los repos de verdad (sergio/takana,
tawasuyu/agora, card, chasqui, cosmos, khipu, llimphi…) y el clon de uno de ellos con 478 commits y
HEAD correcto.
El snapshot de la DB se saca EN CALIENTE y sale consistente: `sqlite3 gitea.db ".backup …"` con el
servidor vivo, 1,5 s para 314 M, `integrity_check` ok, 44 filas en `repository`. Copiar el fichero a
pelo mientras el servidor escribe es justo lo que no hay que hacer.
Tres detalles que sólo aparecen con los datos puestos:
· **El uid del origen no es el del destino.** Los ficheros llegan con su uid NUMÉRICO y el gitea de
gioser es 969, no el 916 que declaraba la receta. O se chownean 2 G —lento, y hay que acordarse—
o gitea no puede leer sus datos, y eso no falla al copiar: falla al arrancar. La receta pasa a
969, alineada con el origen. El ArtifactHash no se mueve (`[[user]]` está fuera de hash_inputs).
· El `app.ini` de gioser escucha en `127.0.0.1:3002` porque allá caddy hace de proxy: la caja sirve,
pero sólo desde dentro. `HTTP_ADDR`/`ROOT_URL` son adaptación, no copiado.
· 🧨 **El `git` de la distro no puede clonar por HTTP**: `git: 'remote-http' is not a git command`.
Medido sobre el artefacto sellado: `git-core/` trae `git-remote-ext`, `git-remote-fd` y
`git-http-backend` (el lado SERVIDOR) y no `git-remote-http`; `strings` da 0 referencias a libcurl
pese a que `curl` está en `[deps] build`. No se había notado porque en el hub se usa el git de
Artix, no el sellado. Un hub nuevo no podría clonar el repo por HTTPS. Re-sellar `git` es raíz de
`perfil.base` ⇒ unidad propia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
El servicio que falla y agota su backoff sólo se podía recuperar reiniciando la máquina entera.
Ya no. Y no hubo que escribir nada: **el cliente existía en tawasuyu y el crate se llama `arje-ctl`**
(el binario, `arjectl`). Buscarlo por `arjectl` no lo encontraba, y de ahí salió mi conclusión falsa
de que había que implementarlo — el protocolo ya traía ListEntes, SpawnCardFromDisk,
StopCardFromDisk, KillEnte y EnteStatus.
`recipes/arjectl.toml` lo construye del MISMO commit que `arje-zero` (98db584f), y no por comodidad:
el bus es un protocolo entre dos binarios y un cliente de otro árbol puede conectar sin entenderse
con el init. Publica sólo `arjectl`; el crate también produce un `systemctl` de camuflaje que acá no
se instala — en una distro sin systemd, ese nombre en el PATH invita a escribir runbooks con el
verbo ajeno.
⚠ EL HUECO QUE SÓLO SE VE USÁNDOLO: el genesis de la seed dice qué arranca AL BOOT, pero
`start`/`restart` usan `SpawnCardFromDisk`, que lee `/etc/arje/cards.d/<label>.json` — y el armado
no lo escribía:
$ arjectl start gitea
Error: arje-zero rechazó: card gitea: No such file or directory
(buscada en /etc/arje/cards.d/gitea.json)
Son dos preguntas distintas —qué arranca solo, y qué se puede encarnar a pedido— y arje las responde
desde sitios distintos. `inyectar-cards.py` escribe ahora los dos árboles, y en `cards.d` escribe
TODAS las cards, no sólo las nuevas: `sshd` viene del product-rootfs y tampoco era relanzable.
Medido con la VM arrancada UNA sola vez: la imagen trae `cards.d/{gitea,sshd}.json` · `list-units`
da PID/CPU/MEM/HILOS/reinicios · poner el `app.ini` + `arjectl start gitea` ⇒ **GET / 200 sin
reiniciar** (uptime 6 min) · `arjectl restart gitea` cambia el PID (118 → 192) y sigue en 200.
⚠ Y un aviso que costó un HTTP intermitente: **`arjectl start` sobre un Ente YA VIVO lo DUPLICA** —
`SpawnCardFromDisk` no deduplica por label y arje le da un ULID nuevo. En gitea el síntoma fue
`unable to lock level db … resource temporarily unavailable` y un `[F]`: dos servidores peleando por
el mismo estado. Para relanzar se usa `restart`, o se mira `list-units` antes. Un `start` idempotente
es trabajo de arje, no de esta imagen.
⚠ Deuda anotada, no barrida: `arjectl` va en `perfil.servidor` porque este frente es el que lo pagó.
TODA imagen de takana corre arje-zero como PID 1 y ninguna se puede operar sin él ⇒ el argumento
para subirlo a `base` es fuerte, y es una línea. Se deja como decisión.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
PID 1 = arje-zero · gitea encarnado por él (ppid=1) · corriendo como uid=916, la cuenta que declara
`[[user]]` · escuchando en :3000 · `GET /` devuelve 200 con <title>takana git</title> · y su
gitea.db creada por él mismo. Primer servicio de PAQUETE que arranca en una imagen de takana: los
que había venían horneados en el bootstrap.
La cadena entera, eslabón por eslabón: [[user]] → /etc/passwd de la imagen · [[service]] →
service-cards → genesis de la seed → arje encarna → setuidgid → sirve.
Queda escrito lo que NO está probado: el app.ini, el usuario del sitio y los datos se pusieron A MANO
en la VM para llegar al 200 — es justo lo que la mudanza tiene que traer de gioser. Y la imagen no
trae `arjectl`: tras poner la config hubo que reiniciar, porque el backoff de restart se agota y no
hay forma de relanzar un ente en caliente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Antes: dos reconstrucciones con las mismas entradas y el mismo lab daban artefactos
distintos. Ahora: `why-differs` da 464 entradas idénticas · 0 divergen.
Hash: 41eea38e… → a72b1626… (arrastra 37 dependientes directos, 39 transitivos).
── CAUSA 1: el AOT de QML compilaba un conjunto DISTINTO de funciones cada vez ─────────
`libKirigamiTemplates.so` cambiaba de tamaño (5.737.344 vs 5.795.072) y difería en 177
símbolos locales, todos `QmlCacheGeneratedCode…__invoke` y concentrados en CUATRO ficheros:
InlineMessage, LinkButton, NavigationTabBar y Badge.
Y esos cuatro son justo los que importan módulos QML HERMANOS del propio proyecto
(`org.kde.kirigami.platform`, `.primitives`, `.controls`). `qmlcachegen` sólo compila a AOT
lo que puede resolver de tipos, y los tipos del hermano salen de su `.qmltypes`, que produce
otro objetivo del mismo build. `src/templates/CMakeLists.txt` no declara `DEPENDENCIES`
ninguna ⇒ con ninja en paralelo es una CARRERA.
Control que sostiene el diagnóstico: `Chip` y `Heading`, del mismo directorio, no divergen —
y `kirigami-addons` y `kquickcharts`, que también llevan QML, reproducen. No es «QML es
no-determinista».
Arreglo: `-j1` en el compile. Orden topológico fijo ⇒ el cachegen ve siempre lo mismo.
NO es throttling (eso va por nice/taskset justamente para no re-hashear): es corrección, y
el re-hash es el precio que se paga a propósito.
Descartadas, con motivo: `-DQT_QML_NO_CACHEGEN=ON` apaga también el bytecode precompilado
(coste de arranque en el escritorio); `--only-bytecode` sería lo quirúrgico pero
`QT_QMLCACHEGEN_ARGUMENTS` es propiedad de objetivo y NO es INHERITED, así que no hay forma
de ponerla desde la línea de órdenes.
── CAUSA 2: el .tar.bz2 de plantillas se armaba sin orden ──────────────────────────────
`kde_package_app_templates` de ECM tiene camino reproducible —`--sort=name --mtime=@
SOURCE_DATE_EPOCH --numeric-owner --owner=0 --group=0`— pero detrás de `if(GNU_TAR_FOUND)`,
que exige que `tar --version` diga «GNU tar». El rootfs del lab es Alpine: `tar` es BUSYBOX
⇒ caía al respaldo `cmake -E tar cvfj`, que ni ordena ni normaliza. El orden lo ponía readdir.
Arreglo: `tar` (GNU tar 1.35, ya en el corpus) entra en `[deps] build`.
Comprobado mirando el artefacto, no deducido:
drwxr-xr-x 0/0 1970-01-01 00:00 ./
-rw-r--r-- 0/0 1970-01-01 00:00 ./CMakeLists.txt
-rw-r--r-- 0/0 1970-01-01 00:00 ./LICENSES/BSD-3-Clause.txt
owner 0/0, mtime al epoch (SOURCE_DATE_EPOCH=1 lo fija el sandbox) y la lista ORDENADA.
`scripts/cosmic/atuq-en-imagen.py` ya no contesta «pinta / no pinta» sino CUÁNTO TARDA: mide cada
captura, imprime `+Ns ██ PRIMERA PINTURA`, acepta `--budget` (def. 420 s) y `--until-paint`, y sale
≠0 si no pintó o si se pasó del presupuesto. El magenta se cuenta CON TOLERANCIA porque `cosmic-idle`
atenúa la pantalla al 46 % a los ~+590 s y un contador por color exacto lo lee como «desapareció».
El número se publica en `docs/state/primera-pintura.json` y `scripts/vigia-imagen.py` lo informa como
sexto dato, marcado como «no lo mide este vigía» (arrancar la imagen son ~15 min sin KVM). Se ACUMULA
una entrada por corrida, no se pisa, y el vigía informa la tasa:
⚠ pintaron 3 de 5 corridas · primera pintura +197…+204s
⚠ Y eso corrige lo que publiqué hace una hora. Con cinco corridas sobre la MISMA imagen: pintó en
tres (+197, +197, +204 s) y NO pintó en dos —una con 900 s de observación—, siendo la 4 y la 5 el
mismo comando. El fallo es INTERMITENTE: ni «nunca pinta» ni «sólo tardaba».
El argumento que me llevó a «tarda» era el MOZ_LOG (`mapped 1` + «has buffer» + WaylandBufferSHM
commiteado). La corrida 5 lo refutó: commiteaba cuadros desde ≤+377 s con la pantalla vacía a los
900 s. Que el cliente se crea visible NO es evidencia de que se vea; la evidencia es el píxel. Queda
escrito en el vigía, al lado de la tasa, para que no se vuelva a usar como prueba.
De paso, las flags nuevas nacen en inglés (CLAUDE.md §4): --as-user, --wait, --budget, --until-paint,
--only-cosmic; los mensajes siguen en castellano.
El SDD 30 sacó las Cards de una constante de Rust y las puso en la receta. Los USUARIOS se quedaron
donde estaban las Cards: `/etc/passwd` de la imagen es `takana_bootstrap::PRODUCT_PASSWD`, un literal
con `root` y `sshd`, y añadir un tercero era editar Rust y recompilar takana.
No es simetría por elegancia, es un servicio que no arranca: `gitea` se niega a correr como root, su
Card hace `setuidgid gitea`, y sin la cuenta arranca, muere y reintenta para siempre con un log que
dice `unknown user` — no «a la imagen le falta una cuenta».
[[user]]
name = "gitea"
uid = 916
home = "/var/lib/gitea"
**El uid se declara, no se asigna**, por la misma razón que el ULID de la Card: un «primero libre a
partir de 1000» hace que dos imágenes del mismo perfil salgan con dueños distintos y el rootfs deje
de reproducir SIN QUE NADA FALLE — los ficheros se ven iguales y `ls -l` dice otro número. Fuera de
`hash_inputs`, medido: el hash de gitea no se movió (`b3:391a613e…` antes y después).
`takana users <recetas…> [--merge <rootfs>]` es el gemelo de `service-cards`, y fusiona **por clave,
no por línea entera** — componer dos veces no duplica, y `grep -q` de la línea completa no serviría
porque la misma cuenta con otro GECOS se leería como nueva. Tres decisiones con su control:
· Una cuenta ya presente con OTRA línea es CONFLICTO y no se pisa: sale ≠0. Pisarla es cambiarle el
uid a ficheros que ya son de alguien, y eso se descubre dentro de la VM.
· Se planea todo y sólo entonces se escribe. Fichero a fichero, un conflicto en `passwd` dejaba el
`group` ya escrito: media cuenta es peor que ninguna, porque parece que está. Comprobado con el
caso exacto — `group` sin la cuenta, `passwd` con otro uid — y los DOS ficheros quedan intactos.
· Un `passwd` ausente es un error, no un fichero a crear: crearlo dejaría una imagen SIN `root`.
La validación rechaza lo que rompe tarde: `root`/`sshd`/`nobody` y sus uids, uid fuera de
100..=65533, `home` relativo y un `:` en cualquier campo — que partiría la línea y fallaría lejos.
Y `--user-paths` NO es `--service-paths` con otro nombre: aquél lista los servicios HABILITADOS,
éste los paquetes INSTALADOS que declaran cuentas. `postgres` instalado y sin levantar necesita su
usuario igual, porque los ficheros de la imagen ya son suyos.
`scripts/servidor-image.sh` lo aplica entre hidratar el perfil y sellar la imagen, y aborta si hay
conflicto: un rootfs con el uid equivocado produce ficheros de un dueño que no existe.
Verde: 6 tests del módulo, core 234, cli 88, bootstrap 42, `targets.py --selftest` 7/7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
`--como-usuario` (atuq pelado, con SU perfil por defecto en la ext4 del disco) reprodujo el síntoma:
300 s de observación, seis capturas, ninguna con ventana. Pero el MOZ_LOG dice otra cosa 30 s después
de la última captura: `mapped 1`, «marked as visible & has buffer», `WaylandBufferSHM` de 1280x696
commiteado y `mIsFullyOccluded 0` — lo mismo que hace cuando pinta.
Segunda corrida, 900 s y captura cada 60 s:
+66s +137s fondo liso
+204s 699458 px magenta — la página, pintada
+268s…+526s 703766 px, estable
La primera pintura cae entre +137 s y +204 s, contra ~+134 s con perfil nuevo en tmpfs; y en la
corrida anterior no había llegado a los +291 s con la máquina más cargada. Bajo TCG «cinco minutos y
dos capturas» no distingue «no pinta» de «todavía no pintó», así que el §6.10.ter queda corregido en
su propio párrafo: de sus cuatro afirmaciones, la cuarta medía la paciencia.
⚠ Trampa nueva, para el que venga a medir la imagen: `cosmic-idle` ATENÚA la pantalla. Entre +526 s y
+590 s sin una sola entrada, los 703766 px de (255,0,255) pasan a 703779 px de (117,0,117) — la misma
ventana al 46 %. Un diff de píxeles o un contador por color exacto lo lee como «desapareció».
kio kcolorscheme kdeclarative krunner kwayland layer-shell-qt kquickimageeditor
kcharselect kruler kdf kfind.
REPRODUCEN: 11 · DERIVA: 0 · NO-DETERMINISMO: 0 · no construyeron: 0
`kio` entra acá y es de las gordas del árbol KDE: reproduce bit a bit.
La receta ya declara su servicio (SDD 30) y `perfil.servidor` lo habilita. El ArtifactHash NO se
mueve (`b3:391a613e…` antes y después): `[[service]]` está fuera de `hash_inputs`.
⚠ Primero, una corrección del commit anterior: declaraba `gitea` en `perfil.servidor` con SEIS
LÍNEAS DE COMENTARIO y sin la línea `"gitea",`. El perfil cargó igual, el grafo no dijo nada y el
paquete simplemente no estaba. Lo cazó `targets.py --services` al resolver el label: «lo declara
gitea, que NO pertenece al perfil». Un comentario que explica una entrada que no existe se lee como
la entrada.
La card no encarna el binario directo: **gitea se niega a correr como root** (`[F] Gitea is not
supposed to be run as root`), así que va por `setuidgid`. Y comprueba dos cosas del SITIO antes de
arrancar, saliendo 78 con un mensaje que las nombra: `/etc/gitea/app.ini` y el usuario `gitea`. Un
gitea sin config no falla — arranca y ofrece el asistente de «crear administrador» a quien pase.
Probado en el worker con el argv exacto y `env -i`, que es lo que arje hace de verdad. Tres fallos
que con una shell normal no se ven NUNCA:
· `exec setuidgid …` a secas ⇒ `sh: exec: line 0: setuidgid: not found`. El `sh` de busybox de la
imagen no trae `FEATURE_SH_STANDALONE`: no despacha sus applets, los busca en `PATH`, y PID 1 no
garantiza ninguno. Todo con ruta absoluta (`/bin/grep`, `/usr/bin/setuidgid`).
· sin `PATH` en el `envp` ⇒ `git not found: executable file not found in $PATH`. **gitea lanza
`git` como subproceso**, y el mensaje se lee como «falta git» con git instalado y raíz de `base`.
Un envp vacío no es «limpio»: es sin PATH.
· sin `cd` ⇒ `fatal: error reading '/root/.git'`. El cwd se hereda y gitea corre `git config` en él.
`Service` no tiene campo `cwd`, así que va en el argv, a la vista.
Con eso: escucha, `GET /` responde **200** y el proceso corre como `gitea`. Las dos guardas
verificadas por separado (78 y su mensaje cada una). El worker quedó limpio y censado: sin usuario,
sin /etc/gitea, sin /var/lib/gitea, sin symlinks y sin procesos.
Y `/etc/gitea` entra en las rutas de la mudanza: el `app.ini` guarda los SECRETOS generados (gitea
los escribe en el propio fichero, que por eso tiene que ser suyo, `gitea:gitea 0660`).
⚠ Lo que queda abierto y está anotado en la receta: **el usuario `gitea` no existe en el producto**.
`/etc/passwd` de la imagen es la constante `takana_bootstrap::PRODUCT_PASSWD` y sólo trae `root` y
`sshd`. El SDD 30 sacó las Cards de una constante de Rust y las puso en la receta; los USUARIOS
siguen donde estaban las Cards. Hasta que se declaren, el usuario llega con los datos del sitio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Hub, KF6 tanda 4: REPRODUCEN 9 · DERIVA 1 · NO-DETERMINISMO 2 · no construyeron 0
Worker, Rust: REPRODUCEN 13 · DERIVA 2 · NO-DETERMINISMO 0
El libro pasa de 104 a 207 entradas en la jornada.
── Los dos que divergen, y por qué importan ──────────────────────────────────────────
`kirigami` — está en `escritorio-kde` y tiene **37 dependientes**. Dos sitios:
· libKirigamiTemplates.so: los símbolos del AOT de QML llevan un contador por fichero
que cambia de corrida a corrida (`…LinkButton_qml3$_28/_38/_58` en A contra
`…Badge_qml3$_88/_98/_108` en B) ⇒ el orden en que se compilan los .qml no está fijado.
· kdevappwizard/templates/kirigami6.tar.bz2: difiere desde el byte 0xa, o sea desde el
primer bloque comprimido — un tar armado sin orden estable.
`syntax-highlighting` — también en `escritorio-kde`. libKF6SyntaxHighlighting.so difiere
en 8.800.310 bytes desde el offset 41 y hasta **el tamaño cambia** (10.345.888 contra
10.345.840). Las cadenas que difieren son datos comprimidos ⇒ es el recurso generado con
las definiciones de sintaxis, otra vez orden de entrada.
Los tres de hoy (con `gocryptfs`) son la MISMA clase: **recurso generado cuyo orden de
entrada nadie fija**. No es el compilador ni el lab: es que la receta deja que el orden lo
decida un `readdir` o el planificador.
── Lo que NO se hace acá, y por qué ──────────────────────────────────────────────────
Arreglarlo toca la receta ⇒ re-hash ⇒ reconstruir kirigami y sus 37 dependientes. Eso es
una decisión de campaña con su coste, no algo que se mete de paso en una verificación.
Los cuatro ejemplares quedan en `store/.divergen/` para poder mirarlos.
── Control que sostiene el hallazgo ──────────────────────────────────────────────────
`kirigami-addons` y `kquickcharts` TAMBIÉN llevan QML y reproducen. O sea que no es «QML
es no-determinista»: es algo propio de esas dos recetas.
La mudanza emite tarjetas de arje y el producto instalado también; quién es dueño del formato no
estaba escrito, así que por omisión son dos repos. Medido hoy:
el tipo canónico existe y está en tawasuyu `shared/card/card-core::Card`
…y VALIDA su versión CARD_SCHEMA_VERSION = 1, comparada al deserializar
emisores dentro de takana 2, en dos lenguajes (bootstrap Rust + arje.py)
deps de card-core en crates/*/Cargo.toml 0
`"schema_version": 1` escrito a mano 5 sitios
arje-absorb usa card_core::Card y ya trae systemd/openrc/runit/
dinit/sysvinit; formatos/ reimplementa DOS en Python
El día que el esquema pase a 2, arje rechaza las tarjetas de takana — y no falla en un test: falla
en el arranque de una caja recién instalada. Hoy coinciden por suerte, no por construcción. Es el
mismo fallo que ya se pagó un piso más abajo, cuando había dos emisores DENTRO de takana y cada uno
tenía un campo distinto mal en la raíz.
Decisión: tawasuyu DEFINE, takana GENERA. Un solo emisor acá y que serialice con el tipo, no con
`format!` — criterio de aceptación negativo: cero literales `"schema_version"` en el árbol. Los
lectores de init ajeno se RETIRAN (absorb ya los tiene), no se mejoran, y mientras convivan hay una
prueba que compara las dos salidas y falla si divergen. Lo que takana aporta —el lector `proc`, que
lee lo VIVO porque `rc-status` miente— va hacia absorb, no en paralelo.
Y la herramienta NO se muda a tawasuyu: la mudanza habla de recetas, perfiles, store y ArtifactHash.
Lo que cruza la frontera es el tipo, no la herramienta.
Dos precondiciones que no se saltan: `card` es uno de los 26 repos sin copia fuera de gioser (hacer
que el bootstrap dependa en compilación de un repo que sólo vive en la máquina que se borra es
convertir un problema conocido en un bloqueo de arranque); y `STAGE1_SEED_CARD` ENTRA EN EL HASH del
bootstrap, así que reserializar con card-core puede mover el baseline del selfhost — se mide con
`takana hash` antes, y si se mueve es una decisión propia. La mudanza no tiene ese problema y puede
adoptar el tipo primero.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
La sospecha salía del propio serial: el arranque del §6.10.ter veía DOS dispositivos DRM
(`drm: card0 card1 renderD128`) y uno nuevo ve uno solo. Sin `-vga none`, QEMU agrega una VGA
estándar además del virtio-gpu, OVMF pinta su GOP ahí y el kernel levanta simpledrm encima: un
compositor con dos tarjetas puede componer en la que el `screendump` no muestra, y eso se ve igual
que «la ventana no aparece». Por eso el guion lleva `VGA=1`, para pedir ese caso por su nombre.
Medido con las dos configuraciones y el mismo lanzamiento:
-vga none → card0 → 703766 px magenta
VGA=1 → card0 card1 → 703766 px magenta
El mismo número al píxel. La ventana aparece —panel, dock y el navegador con su barra lateral—,
`nsWindow::Create() Toplevel` está en el MOZ_LOG, la superficie se mapea y `mIsFullyOccluded 0`, con
`WaylandBufferSHM` de 1280x696, el mismo camino de buffers que bajo sway.
⇒ la segunda pantalla no es la causa. Queda una sola diferencia con aquella corrida: cómo se lanzó
el navegador. Acá va con perfil nuevo y MOZ_ENABLE_WAYLAND puesto; allá fue un `atuq` pelado con su
perfil por defecto tecleado en el serial. Para eso está `--como-usuario`, que es la corrida que sigue.
⚠ Dos trampas del arnés, medidas y escritas en el guion: el terminal hace ECO de lo que se le
escribe, así que una marca de fin literal se lee en el eco y las órdenes se pisan; y `cmd & ; echo`
es error de sintaxis en ash ⇒ el navegador no se lanzó y la corrida siguió dando capturas de un
escritorio vacío, que se leen igual que el fallo que se investiga.
Deja escrito lo de hoy donde ya vivía el diagnóstico de esta clase de fallo (§9):
el aprovisionamiento de `go` en el worker SE HIZO el 2026-08-28 y el renombre lo
deshizo el 2026-09-09 sin que nada fallara. Con el método de detección (varias
muriendo en segundos = sistémico; leer el error de UNA antes de contar deuda), la
comprobación en los dos sentidos, y las cuatro sondas de `pgrep` que el mismo
renombre dejó ciegas.
Incluye el no-determinismo de `gocryptfs` con la hipótesis REFUTADA anotada como
tal: no es el `cgo` (sq y usql también lo usan y reproducen) ni la ruta aleatoria
del temporal (las cadenas son idénticas y difieren 1.874.136 bytes, desde .rodata).
Queda abierto y sin causa; no está en ningún perfil, así que no bloquea imágenes.
Hub, KDE Frameworks tanda 3: kglobalaccel kpackage kconfigwidgets kiconthemes
ktextwidgets kxmlgui kparts kcmutils knotifications kdeclarative ksvg kwallet.
REPRODUCEN: 12 · DERIVA: 0 · NO-DETERMINISMO: 0 · no construyeron: 0
Worker (ya con `go` arreglado), familia Go:
REPRODUCEN: 15 · DERIVA: 1 · NO-DETERMINISMO: 1 · no construyeron: 0
· `checkmake` — la que antes «no construía» — REPRODUCE. Cierra el arreglo del enlace
de `go`: no estaba rota, faltaba el binario en el PATH del host.
· `cilium-cli` DERIVA: el artefacto guardado era viejo; las dos reconstrucciones
coinciden entre sí ⇒ se movió el mundo, no la receta. No es un fallo.
· `gocryptfs` NO-DETERMINISMO, y es el primero de verdad del frente Go.
La causa de gocryptfs, con los dos ejemplares guardados en `store/.divergen/`:
usr/bin/gocryptfs ELF, difieren [.text .rodata .data .debug_*]
· .debug_str sólo en A: …/.gotmp/go-build3432240771/b141
· .debug_str sólo en B: …/.gotmp/go-build2186295412/b141
El directorio temporal `go-build<aleatorio>` queda horneado en el DWARF. Hipótesis a
comprobar, no conclusión: sólo pasa con `cgo = true`, porque cgo genera fuentes C en ese
temporal y su ruta real entra en la info de depuración; con CGO off no hay fuentes
generadas y las 15 restantes reproducen. En el corpus hay exactamente cuatro recetas Go
con cgo: gitea, gocryptfs, sq y usql — ninguna verificada todavía. Se miden antes de
afirmar nada.
La mudanza de gioser pasa por levantar su gitea del otro lado — 28 repos, 26 sin copia fuera. El
corpus ya tenía `gitea` sellado (1.26.4, `b3:35bb4f04…`) y NO sirve, medido contra el artefacto:
$ gitea --config app.ini migrate
Error: sqlite3 requires: -tags sqlite,sqlite_unlock_notify
this Gitea binary was not built with SQLite3 support
Construía, reproducía y no podía abrir la sqlite en la que gioser guarda TODO (`DB_TYPE = sqlite3`).
Y `gitea --version` decía `version development`. Es subcomando-sin-driver un piso más abajo.
Tres cambios, y el tercero es el que no se ve venir:
· Pin 1.27.0 (gioser corre 1.27.0). No es cosmético: gitea migra el esquema hacia adelante y se
planta si la DB trae migraciones más nuevas que el binario, así que el pin va por delante del
origen, nunca por detrás.
· Tags `sqlite,sqlite_unlock_notify` (⇒ `cgo = true`, el driver de mattn es C) y `bindata`, que
embebe `public/` y `templates/`: sin él la receta publica sólo el ejecutable y el servidor
arrancaría sin UI.
· La fuente pasa de `repo`+`commit` al TARBALL DE RELEASE. El clon no alcanza: la UI se compila
con Node y las deps Go no están vendoreadas en git. El `gitea-src-1.27.0.tar.gz` trae las dos
cosas hechas — medido: `public/assets/` 764, `vendor/` 11246, `templates/` 663 — así que
construye con el lab tal cual, sin Node y sin red. La URL es locator (ADR 0013); ancla el sha256.
Nuevo hash: `b3:391a613e…`. Declarado en `perfil.servidor` como paquete y NO en `servicios`:
arrancarlo necesita el [[service]] del SDD 30 con su app.ini, y declarar un arranque que no existe
sería la mentira que ese campo existe para no tener.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Tanda 2 de la familia KDE Frameworks (hub): kbookmarks kcolorscheme kcompletion
kdnssd kholidays kunitconversion kauth kpty solid sonnet prison kservice.
REPRODUCEN: 12 · DERIVA: 0 · NO-DETERMINISMO: 0 · no construyeron: 0
Y aparte: el worker tenía su propio libro (`work/repro-worker.tsv`, fuera de la siembra)
con 13 verificaciones del 2026-09-05 que nunca volvieron al hub. Cobertura medida y
perdida. Importadas 10.
Las otras 3 NO se importan, y el motivo es el que justifica que la clave del libro sea
(receta, ArtifactHash) y no la receta sola: su hash cambió desde entonces, así que la
medición ya no habla del artefacto de hoy.
· atuq, bzip2 — reproducían, pero a un hash superado
· aichat — decía NO-DETERMINISMO a `c40a15bd…`; el hash vigente es `7a515b0a…`
y a ÉSE el propio worker le midió «reproduce». Importar por nombre
habría arrastrado un no-determinismo que ya no existe.
El §6.10.ter dejó dos variables cambiando a la vez (compositor y arranque real) y ninguna medición
que las separe. `scripts/cosmic/atuq-en-cosmic.sh` saca la primera de encima en ~2 min por vuelta,
sin QEMU y sin imagen: sway headless de andamio, cosmic-comp nesteado encima por winit, atuq adentro,
y `grim` capturando del lado de sway. El `--control` corre EL MISMO binario de atuq —el del cierre
hidratado para la imagen— directo sobre sway.
Las dos capturas coinciden al píxel en la región de la página (586331 px magenta, mismo bbox), y no
es que el navegador se escape a sway: antes de lanzarlo, la ventana de cosmic-comp ocupa 1276x637 en
ese mismo rectángulo, y el MOZ_LOG del widget ve `mode output size 1276 x 637` con cosmic contra
`1280 x 720` en el control. Las dos corridas enteras difieren en 1130 píxeles, todos en las barras
de título.
⇒ cosmic-comp no es el que impide la ventana. Queda como variable el arranque de verdad: kms/DRM
sobre virtio-gpu en vez de winit, el PID1 de arje, el seat.
De paso queda anotado el camino de buffers cuando SÍ funciona —`WaylandBufferSHM`, memoria
compartida y no dmabuf—, que es la línea de base contra la que comparar la traza de la imagen.
Y `scripts/cosmic/atuq-en-imagen.py` para medir allá: arranca la imagen, maneja el serial, lanza el
navegador con el mismo MOZ_LOG y pide capturas por QMP. La corrida del §6.10.ter fue a mano y no
dejó un solo log que se pueda releer.
Barrido por FAMILIA (KDE Frameworks tier 1, todos CMake), que es lo que hace
legible el censo: si el fallo fuera del toolchain se concentraría acá y saltaría
a la vista. karchive kcodecs kconfig kdbusaddons kguiaddons ki18n kidletime
kitemmodels kplotting kwidgetsaddons kwindowsystem kcrash.
REPRODUCEN: 12 · DERIVA: 0 · NO-DETERMINISMO: 0 · no construyeron: 0
Los 12 además CONSTRUYEN hoy, que es el otro hallazgo del verificador: `sealed`
sólo dice que alguien los construyó alguna vez con algún lab, y el lab rueda.