# SDD 33 — Versionar takana: cómo se llama una versión, qué reloj mira qué, y qué se cambia solo > **Estado: PROPUESTA, con DOS piezas implementadas y medidas.** > — `scripts/fuentes/upstream-vigia.py` (2026-09-23), el reloj de río arriba. **Cableado al cron el > 2026-10-07** con puerta diaria: hasta ese día había corrido UNA vez a mano y su informe llevaba > catorce días congelado, que es el modo de falla que §2.1 describe para los guardianes. > Extendido el mismo día a 196 de los 625 tarballs (§5). > — `scripts/fuentes/bump-repo.py` (2026-10-07), el **aplicador atómico por repositorio** del §3.1, > con la política del §3.2 metida EN EL COMANDO: con un repo de terceros se niega (rc=2) salvo > `--third-party`. Dos bumps aplicados: tawasuyu (35 recetas, 35/35 selladas) y nuestro propio > repo (5 recetas que estaban en **cinco commits distintos**). > El resto está decidido y no construido —§4, rearmar los medios por el CIERRE— y se dice cuál es cuál. ## 0. La pregunta > *¿cómo ir versionando esto? hay las actualizaciones desde los orígenes que hay que adaptar las > recetas, por ejemplo ya está kernel 7.3. y lo que voy desarrollando de tawasuyu que avanza rápido.* Son **tres** preguntas y conviene no mezclarlas, porque cada una se contesta distinto: 1. **Cómo se LLAMA una versión** de takana — hoy no se llama de ninguna manera. 2. **Quién mira qué se movió río arriba**, y cada cuánto. 3. **Qué se puede cambiar solo** y qué no. Ésta es la única con filo. ## 1. Una versión ya existe, y no es un número La identidad real de una imagen **ya está**: es el hash de la clausura de su perfil. No se puede falsificar, no depende de quién la armó y dos imágenes con el mismo hash son la misma imagen. Eso es mejor que cualquier número, y es lo que el sistema usa para todo. Lo que falta es un nombre que una persona pueda decir en voz alta. Propuesta: | | | |---|---| | **identidad** | `b3:` — exacta, la que verifica | | **nombre** | `takana AÑO.MES[.n]` — `takana 2026.09`, y `2026.09.2` si hay más de una en el mes | | **dónde vive** | en la imagen (`/etc/os-release`), en el nombre del fichero publicado, y lo imprime el instalador antes de tocar el disco | ⚠ **El nombre NO es la versión: es una etiqueta sobre ella.** Un fichero llamado `takana-2026.09-mirada.sh` tiene que decir de qué clausura salió, porque el día que alguien reconstruya para comprobar, lo que compara es el hash. Publicar sólo el nombre es la forma barata de que el sitio empiece a mentir — que es lo que ya pasó con un sha256 pegado a mano en la página. ## 2. Tres relojes, no dos, y ya hay uno andando | reloj | cada | qué hace | estado | |---|---|---|---| | **cosecha** | 30 min | trae del worker lo que se selló, siembra recetas, regenera el estado y commitea | **anda** (`scripts/farm/cosecha-cron.sh`) | | **río arriba** | 1 día | pregunta a cada fuente si hay algo más nuevo que el pin, y deja un informe | **la pieza implementada hoy** | | **medios** | cuando el cierre cambie | rearma imágenes e instaladores y publica | **anda el bucle entero** (§4): el cierre se publica cada ciclo, la imagen lo declara en su `os-release` y `imagen-rancia.py` compara. Falta que haya imágenes con las que comparar | ### 2.1 Río arriba — `scripts/fuentes/upstream-vigia.py` Pregunta lo que `fuentes-vigia.sh` no pregunta. Aquél mira si la fuente **sigue viva**; éste mira si hay **algo más nuevo que lo que tenemos clavado**. Se confunden, y son las dos mitades: una fuente viva puede llevar dos años sin tocarse, y una que se movió ayer puede desaparecer mañana. Medido el 2026-09-23, primera corrida completa: ``` 660 recetas con repo git · 17 s 462 con un tag más nuevo que el pin 606 con la rama movida desde el pin 625 recetas pinean un TARBALL y esta vigía no las mira ``` **Por qué sólo git, y por qué eso ya es la mitad.** `git ls-remote` funciona contra cualquier servidor sin API, sin token y sin clonar — es lo mismo que ya hace `takana pin`. Con eso se cubren las 660 que pinean `repo` + `commit`. **Se cuentan y se dicen**, no se adivinan: 625 recetas fuera del radar es un dato, no un silencio. ⚠ **Y lo que seguía era falso para el grueso. Corregido el 2026-10-07.** Decía: «un tarball no tiene equivalente general: saber si hay una versión nueva pide una pista por receta». Cierto en general, falso para la mayor parte — y nadie volvió a medirlo en catorce días: ``` 2026-10-07: 196 de github cubiertas con el ls-remote que ya estaba · 429 fuera 2026-10-09: 223 de forja (github+gitlab) · 402 por índice HTTP · 44 fuera 625 tarballs → 581 contestados en 104 s con 236 peticiones y con la FECHA del índice: 150 verificados · 230 indicios respaldados · 30 sin fecha · 5 ramas abandonadas descartadas ``` `.../archive/refs/tags/v1.2.3.tar.gz` y `.../releases/download/1.10/sway-1.10.tar.gz` llevan dentro todo lo que hacía falta. No se necesitaba API, ni token, ni campo nuevo: se necesitaba **leer la URL**. ⇒ *Una frase que cierra una línea de trabajo hay que volver a medirla: el límite puede haber sido del primer intento y no del problema.* ⚠ **Y la primera versión de esa cobertura era un generador de ruido**, que es la otra mitad de la lección. Comparar contra el tag más nuevo del repo sin mirar su SERIE da basura en cuanto upstream publica varias — **9 falsos de 91, medidos**: `cronie` baja `cronie-1.7.2` y decía «hay v4.3»; `bash-completion` baja `2.18.0` y decía «hay 20060301-alt06». La comparación se acota al mismo prefijo no numérico y la misma cantidad de componentes, **sesgando hacia el silencio**, y lo descartado se cuenta y se dice (11 sin serie comparable, 4 que bajan por sha, 50 sin tags). Un guardián cuyos hallazgos son casi todos falsos no se apaga: se deja de leer, y entonces tampoco se ve el hallazgo verdadero. ⚠ **Sale 0 siempre.** Un guardián que falla porque upstream sacó una versión nueva grita todos los días y en una semana nadie lo mira ([[guardianes-el-ruido-los-mata]]). ## 3. Qué se cambia solo — la única pregunta con filo > **Se automatiza el pin de lo NUESTRO. No se automatiza el de nadie más.** ### 3.1 tawasuyu: sí, y de a un repo entero Son **37 recetas sobre el MISMO monorepo**, y ahí está la trampa: bumpear 37 pins de a uno da una imagen con 37 commits distintos del mismo árbol — mezcla de versiones que nunca existió junta y que nadie probó. El bump tiene que ser **atómico por repositorio**: todas las recetas que comparten `repo` saltan al mismo commit, o no salta ninguna. Es defendible automatizarlo porque es código propio, la granja lo reconstruye y los guardianes ven la rotura. Lo que **no** se automatiza ni ahí es publicar: un bump que compila no es un bump que arranca. ### 3.2 Terceros: no, y el kernel 7.3 es el ejemplo Una versión nueva de upstream puede cambiar el sistema de build, las banderas, las dependencias y el resultado. Aplicarla sola es cambiar la receta sin leerla. El kernel lo muestra entero: pasar de 6.16 a 7.3 no es cambiar un número. Toca el contrato de kconfig (SDD 22), las opciones que nuestras imágenes exigen (`CONFIG_KEXEC_FILE`, el EFI-stub, el `CONFIG_CMDLINE` horneado del que depende el arranque del USB), los drivers de las máquinas que ya validamos, y las tasas del SDD 25. **Es una unidad de trabajo, no una línea de cron.** ⇒ Para terceros la vigía **propone**: deja el informe, y quien decide aplica con `takana pin` y una construcción. Proponer y aplicar son verbos distintos y conviene que también sean comandos distintos. ## 4. Cuándo rearmar los medios: por el CIERRE, no por el calendario Un cron semanal que rearma las imágenes gasta horas de worker para producir, casi siempre, **los mismos bytes**. El disparador correcto no es el tiempo: es que la clausura del perfil haya cambiado. ``` por cada perfil publicado: h = hash de la clausura del perfil (barato: `takana hash`, sin construir) si h ≠ el hash de la imagen publicada: rearmar, probar con los tres arneses, publicar ``` Y **publicar es lo último**, después de los arneses, nunca antes: hoy son 25 comprobaciones del banco, 13 del salto y 13 del USB, y las tres corren sin intervención. ✅ **La primera mitad está construida** (2026-10-10): `scripts/cierre-perfiles.py`, cableado al latido y publicando `docs/state/cierres.json`. Ese `h` **no existía** — el grafo sabía el hash de cada artefacto y nadie resumía el conjunto — y es lo que convertía este §4 en pseudocódigo inejecutable. No construye nada: lee los grafos que el latido acaba de regenerar, en menos de un segundo. ⇒ **el `git log` de `cierres.json` ES la señal «este perfil se movió, su imagen quedó rancia»**, que hasta hoy no se podía saber sin rearmar para averiguarlo. ⚠ **De qué conjunto, que es la parte que se puede errar sin enterarse: del cierre de IMAGEN.** La primera versión sumó las herramientas de `[solo-build]` —`llvm21`, `go`, `python3`, `cmake`, `perl`—, que no van en la imagen porque **la imagen no se construye a sí misma**. Entre 17 y 35 nodos de más por perfil, en pares (cola,nombre): ``` perfil build∪runtime imagen solo-build de más base 86 69 17 cli 153 133 20 escritorio-cosmic 296 261 35 escritorio-gnome 369 337 32 escritorio-kde 445 411 34 escritorio-mirada 41 23 18 escritorio-sway 282 249 33 servidor 186 166 20 servidor-minimo 117 99 18 ``` Con el conjunto equivocado, tocar un compilador movía el digest y decía «rearmá» cuando los bytes de la imagen no cambian — **un disparador que se enciende por algo que no entra en la imagen es el calendario que este §4 quiere evitar.** La membresía sale de `yupana.membresia_imagen()`, la misma vista que `hydrate-profile.py` proyecta al FHS: una sola fuente para «qué lleva esta imagen». Tres clases de miembro, y las tres se nombran en el informe en vez de desaparecer en el número: los **propios** entran por su hash; los **`ajeno`** (xwayland, rust-toolchain-bin, steam-runtime-sniper) y los **`wanted`** —declarados por el perfil y sin receta que los provea— entran por NOMBRE con su marca, porque no se les puede pedir hash y no aportan bytes, pero el día que alguien les escriba la receta la marca pasa a hash y el digest se mueve. `escritorio-kde` declara `wanted/xwayland`, y el informe lo dice. Y un cierre **incompleto** no se hashea: un digest sobre una clausura parcial diría que la imagen cambió cuando lo que cambió es cuánto sabemos. Los hashes se indexan por **(cola, nombre)**, porque `openssl` es una receta distinta en `corpus` y en `incoming-kde`; comprobado que los cinco grafos no se contradicen (1.288 pares, 0 conflictos), y si algún día lo hacen el informe lo grita en vez de elegir uno. ✅ **Y la segunda mitad también, el mismo día: la imagen DECLARA de qué clausura salió.** `hydrate-profile.py` escribe `/etc/os-release` con `TAKANA_PERFIL`, `TAKANA_NODOS` y `TAKANA_CIERRE`. Antes **ninguna imagen declaraba nada** — no se escribía ni un `os-release`; los aciertos de `grep` en el repo eran todos lecturas para detectar el anfitrión. Va en ese guion y no en cada script de imagen por el mismo motivo que el bit de privilegio: por ahí pasan TODAS las imágenes de perfil, y un paso que hay que acordarse de invocar se olvida. ``` NAME="takana" ID=takana PRETTY_NAME="takana · metal-tigerlake" HOME_URL="https://takana-os.net" TAKANA_PERFIL=metal-tigerlake TAKANA_NODOS=5 TAKANA_CIERRE=sha256:5fed4348345bc86498f54c2266d3a7118e646c1bc1685d3baa5f0f822631cac9 ``` **El digest lo calcula la MISMA función que la vigía** (`digest_de`), sobre lo que el guion acaba de proyectar. Y eso no es celo: la primera hidratación real dio **dos números distintos** —la vigía `5fed4348…`, la imagen `17de5d7e…`— con los cinco hashes idénticos uno a uno. La causa era el prefijo: el grafo guarda `b3:7373…` y `takana hash` lo da pelado. Mismo hash, cadena distinta, digest distinto. ⇒ **la función normaliza adentro**, porque una función compartida que exige un formato convenido de palabra ya son dos implementaciones otra vez. Lo encontró hidratar de verdad y comparar, no leer el código. ⚠ **Nada derivado del reloj entra en el fichero**, o la imagen deja de ser reproducible: una fecha cambiaría sus bytes cada mes sin que la clausura se mueva — la mentira que el §1 señala. El nombre legible es una ETIQUETA y entra sólo con `--version`. Comprobado en los dos sentidos: con `--version 2026.10` aparecen `VERSION`/`VERSION_ID` y **el digest no cambia**; y dos hidrataciones seguidas dan un `os-release` **byte a byte idéntico**. ⚠ **Un rootfs con artefactos faltantes NO declara digest**: pone `TAKANA_CIERRE=incompleto` y cuántos faltan. Un número con pinta de identidad sobre una imagen que no es la clausura que dice ser es lo peor de los dos mundos, y el §4 compararía contra una mentira. ✅ **Y el paso que las junta — porque dos mitades que no se encuentran son dos ficheros, no un disparador.** `scripts/imagen-rancia.py` lee el `TAKANA_CIERRE` de cada imagen y lo compara con el vigente de su perfil; dice cuáles rearmar y no rearma. Cableado al latido con puerta diaria. Cinco caminos, los cinco probados: | | | |---|---| | **al día** | hidratando `metal-tigerlake` de verdad y comparando ⇒ `sha256:5fed4348…` | | **rancia** | perturbando el hash de `intel-ucode` en el grafo ⇒ lleva `5fed4348…`, hoy `d6fef70f…` | | **rancia** | imagen que declara un digest viejo | | **incompleta** | armada con `--allow-missing` ⇒ rearmar | | **muda** | imagen anterior al §4 ⇒ «no se puede saber», **no** «al día» | ⚠ Lo último no es un detalle de redacción: **tratar la ausencia de declaración como conformidad es la forma exacta de que un medio rancio pase por bueno.** Y el control de rancidez destapó algo que no se buscaba: saltó la guarda de **hashes contradictorios entre grafos** —perturbar uno solo deja a los otros cuatro con el hash viejo— así que dijo RANCIA *y* avisó de que la entrada era inconsistente. Una guarda que nunca había disparado, haciendo su trabajo. ⇒ **El §4 corre de punta a punta.** Lo que queda no es una pieza que falte: es que no hay imágenes publicadas todavía (el sitio no ofrece descargas; en `/srv/repo` hay paquetes `.tkn`). El registro `docs/state/imagenes.txt` está vacío a propósito, y existe para que el día que se arme una, vigilarla sea **añadir una línea** en vez de acordarse de que esto existe. ## 5. Lo que este documento NO decide - ~~**La pista por receta para los tarballs** (625)~~ → **la premisa era falsa para el grueso, y 196 ya están cubiertas** *(2026-10-07)*. Decía que «hace falta un campo tipo `[source.upstream]` con dónde mirar y qué patrón». Cierto en general; falso para la mayor parte: **196 de los 625 tarballs son de `github.com`**, y en esas URLs el repo y la versión **ya están escritos** (`.../archive/refs/tags/v1.2.3.tar.gz`, `.../releases/download/1.10/sway-1.10.tar.gz`), así que el mismo `git ls-remote --tags` del §2.1 las cubre sin API, sin token y sin pista por receta. Primera corrida: **82 bajan una versión más vieja que la publicada**. ⇒ *Una frase que cierra una línea de trabajo hay que volver a medirla: el límite puede haber sido del primer intento y no del problema.* **Y las 429 restantes también se cerraron, dos días después** *(2026-10-09)*. No pedían una pista por receta: pedían **un listado por host**, y los hosts estaban concentrados. Dos mecanismos, y el primero tampoco era nuevo: `gitlab.freedesktop.org` (26) es una **forja git** —sus URLs `/-/archive//…` llevan el repo escrito, igual que github— y el resto son **índices de directorio HTTP**, que un sondeador genérico lee: 402 recetas con **236 peticiones** gracias a la caché por directorio (las 108 de KDE comparten 13; las 29 de xorg, 3). Hace dos sondeos, porque un solo nivel no alcanza: un `-` en su propio directorio (ftp.gnu.org, xorg, kernel.org) **y una SERIE más nueva** cuando la versión vive en un segmento de la ruta (`stable/frameworks/6.27/`, `sources/libadwaita/1.7/`) — ahí mirar sólo el directorio propio diría «al día» para siempre. ``` 625 tarballs → 581 contestados · 44 sin mirar (eran 625 ciegos el 2026-10-07) 223 de forja → 150 con versión nueva, 1 sin contestar 402 por índice → 265 con un número mayor ``` ⚠ **Y un bug propio que valía 53 recetas:** el regex de tags exigía que el tag empezara por dígito, así que descartaba `llvmorg-18.1.8`, `R_2_6_0`, `fuse-3.16.2`, `release-74-2` — **53 salían «sin-tags» contra repos que tienen cientos**. El regex laxo se usa SÓLO en el camino del tarball, donde la URL dice qué forma buscar; en el de `repo`+`commit` la estrechez es la protección y aflojarla cambiaría `al_dia` en 660 recetas que hoy se miden bien. - ~~**Qué hacer con los 265 INDICIOS**~~ → **resuelto el mismo día con la FECHA del índice, y la pieza trae su propio falso negativo medido.** El problema era real: un índice contesta «¿hay un número mayor?», que no es «hay una release más nueva». `gtk3` baja de `sources/gtk+/3.24/` y ese directorio todavía guarda `3.89`…`3.96`, las alfas de GTK4; `3.94 > 3.24` es cierto y semánticamente falso, y ninguna regla numérica lo distingue. **La fecha sí:** ``` sources/gtk+/ 3.24/ 2024-Jul-10 ← lo que usamos 3.94/ 2018-Jun-26 ← seis años más viejo ⇒ rama muerta ``` La regla es la que upstream no puede violar sin querer: *nadie vuelve a tocar una rama abandonada*. Hay tres formatos de fecha en tres servidores y hay que cubrir los tres. ⚠ **Y el primer intento era demasiado estricto, con un falso negativo que vale como advertencia general:** descartaba el candidato en cuanto su fecha fuera anterior a la nuestra, y entonces `glib 2.90` —serie nueva del 2026-Oct-06, con 2.90.0 y 2.90.1 dentro— quedaba tapada porque `2.88` recibió el parche 2.88.4 **dos días después**. Una rama que sigue recibiendo parches es normal; una abandonada lleva años quieta. El umbral de un año queda lejos de los dos bordes (dos días / seis años), así que no está calibrado contra ningún caso concreto. ⇒ Lo que esto compra no es quitar 5 falsos, es **poder decir en qué confiar**: de 260 hallazgos, **230 con la fecha respaldándolos** y 30 sin fecha comprobable. Nunca se descarta por FALTA de fecha — eso sería perder información real por no tener la corroboración — y los niveles van separados en el resumen, porque mezclarlos haría que los 230 buenos se tiraran con los 30 dudosos. - **Los 44 que no contesta nadie**, con nombre: 8 de `files.pythonhosted.org` (rutas con hash ⇒ pide la API de PyPI), 5 de `downloads.sourceforge.net` (descarga por redirección), 5 que pinean por sha a propósito, 3 con `no-hay-upstream.invalid` —que es un marcador NUESTRO y está bien así—, 3 de `static.rust-lang.org`, y una cola de dos en dos. - **Cuándo re-pinear `recipes/takana.toml`**, que pinea el repo que la contiene. El bump atómico del 2026-10-07 dejó las 5 recetas de nuestro repo en el mismo commit —estaban en **cinco distintos**— pero esa receta vuelve a quedar por detrás en el commit siguiente, el del propio bump. Perseguir «al día» no tiene sentido; lo que vale es que las cinco compartan commit. **Re-pinear es una decisión de release, y no está tomada.** - **Si el nombre lleva mes o semana.** Depende de cada cuánto se publique de verdad, y eso todavía no se sabe porque hoy se publica a mano. - **Firmar lo publicado.** Los `.sha256` de al lado comprueban la descarga, no la procedencia. El repo de paquetes ya tiene firma (ADR 0014); las imágenes no, y son lo que más se copia.