# Plan — barrido de perfiles y servicios, MEDIDO **Fecha:** 2026-09-13 · **Catálogo contra el que se midió:** 905 nodos del corpus (903 recetas selladas + 2 ajenos) y los cuatro grafos de cola · **Instrumento:** el campo `perfiles` de `docs/state/build-state*.json` cruzado con `dependientes_total`, más el `[[service]]` de las recetas. No se contó nada a mano. ## Por qué existe este documento Salió de una pregunta lateral. Al declarar doce recetas nuevas en `targets.toml` hubo que mirar qué había ya declarado, y el número no se parecía a lo que cualquiera habría dicho de memoria: > **De 905 nodos, 631 no estaban en NINGÚN perfil.** El 73 % del catálogo estaba sellado, íntegro, > reproducible… y no viajaba en ninguna imagen. Lo grave no es el número: es que **nada lo decía**. `drenaje.json` daba deuda 0 —y era cierto, no hay nada roto—. La métrica de perfil daba N/N —y también era cierta, porque mide la clausura de lo **declarado** y no puede ver lo que falta en la declaración. Es la lección de `foot` (un perfil 121/121 sin emulador de terminal) aplicada al catálogo entero, y llevaba meses ahí. ## El método, para poder repetirlo Tres preguntas encadenadas, y el orden importa porque cada una descarta ruido de la siguiente: 1. **¿Está en algún perfil?** → campo `perfiles` de los cinco `build-state*.json`. Hay que mirar los cinco: una receta del corpus puede estar declarada desde una cola. 2. **¿Podría llegar por la clausura de otra?** → `dependientes_total > 0`. Una librería sin perfil NO es un hueco: llega como dep de quien la usa. **El hueco son las HOJAS**: si nadie depende de ellas y ningún perfil las declara, no hay camino por el que entren a una imagen. 3. **¿Fue curada alguna vez?** → la cabecera del importador. Las recetas de `import-nix` / `import-alpine` traen literalmente «PUNTO DE PARTIDA, no final». Es una marca objetiva y parte el montón en dos mitades con historias distintas. ``` 905 nodos ├── 274 en algún perfil └── 631 sin perfil ├── 11 con dependientes → llegan por clausura ajena, NO son hueco └── 620 HOJAS → sin camino a ninguna imagen ├── 529 volcado crudo del importador (328 Go · 195 Rust · 6 C) └── 91 trabajadas a mano ← éstas se triaron una por una ``` ## La política que faltaba escribir: **catálogo ≠ imagen** La tentación al ver «620 hojas sin perfil» es declararlas todas. Sería peor que no haber mirado: 355 de ellas son herramientas Go de nube y desarrollo importadas en tanda, y meter `kubectl`, `flyctl` y `chartmuseum` en la imagen de escritorio de alguien no arregla nada — engorda cuatro imágenes para servir a nadie. Lo que faltaba no era declarar: era **decir en voz alta que el catálogo tiene dos niveles**, cosa que el repo ya hacía de hecho y no había escrito en ningún lado. | nivel | qué es | cómo llega a una máquina | |---|---|---| | **imagen** | lo que un perfil declara, más su clausura | viene horneado; es lo que arranca | | **catálogo** | todo lo demás que el repo sabe construir y tiene sellado | `takana install ` desde el repo | ⇒ **No estar en un perfil no es deuda.** Es deuda sólo cuando lo que falta es una **capacidad**: algo que la imagen, tal como está, no puede hacer. Ese es el criterio con el que se triaron las 91, y es el que hay que aplicar la próxima vez. ## El triaje de las 91, completo Las seis clases suman 91 exactas; ninguna receta quedó sin clasificar. ### A · Declaradas en este barrido (9) | receta | dónde | por qué | |---|---|---| | **`wpa_supplicant`** | `base` | **la imagen no podía asociarse a un WiFi** — ver abajo | | `wlr-randr` | `escritorio-sway` | no había con qué cambiar resolución ni rotar una pantalla | | `nano` | `cli` | `base` sólo trae `vim` | | `tig` | `cli` | git en TUI | | `patch` | `cli` | aplicar un diff a mano; userland clásico que faltaba | | `socat` | `cli` | sockets y diagnóstico de red | | `pigz` | `cli` | gzip en paralelo | | `miller` | `cli` | `jq` para CSV/TSV — el hueco de datos tabulares | | `dwarves` | `cli` | `pahole`: esta distro compila su propio kernel | ### B · Del proyecto — entran por otro camino, no por perfil (19) `arje-zero`, `arje-absorb`, `arje-installer`, `arje-packager`, `arje-getty-stub`, `arje-net-bring-up`, `arje-zero-attest`, `hammerd`, `hammer-edit`, `agora-cli`, `dominium-cli`, `cosmos-cli`, `tinkuy-sim`, `llimphi-counter`, y los cinco kernels (`linux`, `linux-generic`, `linux-metal`, `linux-metal-dual`, `linux-metal-kexec`). El init y el kernel los hornea el `product-rootfs`; los binarios de la suite viven en tawasuyu. Que estén fuera de los perfiles es correcto — **pero conviene que esté escrito**, porque «`arje-zero` no está en ningún perfil» se lee como alarma hasta que alguien explica por qué no lo estará nunca. ### C · Andamios y pruebas (6) `adwaita-hello`, `gtk4-hello`, `sourceview-hello`, `poppler-render-check`, `firefox-instrumentado`, `mesa-llvmpipe`. Existen para probar una cadena o para alimentar un PGO. No van a una imagen y no deberían aparecer nunca en este recuento como deuda. ### D · Librerías sin consumidor todavía (10) `brotli`, `libarchive`, `libsodium`, `libssh2`, `libuv`, `lz4`, `zlib-ng`, `musl`, `bwrap`, `crun`. ⚠ **Éstas son las únicas de las 620 que merecen una segunda mirada por sí mismas**, y no por falta de perfil: son librerías con **cero dependientes**. Alguien las escribió para algo y ese algo no llegó, o llegó y resolvió contra otra. Un `libuv` sellado que nadie usa es trabajo hecho y no cobrado — `libuv` es, por ejemplo, dep de `neovim`, que el catálogo no tiene. ### E · Decisión abierta — NO se declaran de oficio (17) Cada una compite con algo ya elegido, o enciende una función que nadie pidió. Declararlas por reflejo sería tomar una decisión de distro dentro de un barrido de limpieza: | receta(s) | con qué compite / qué decide | |---|---| | `openrc` | **compite con `arje-zero`**: los inits no se apilan | | `uutils`, `coreutils`, `diffutils`, `findutils`, `findutils-xargs`, `grep` | compiten con los **applets de busybox** que hoy usan las imágenes. Reemplazarlos es una decisión de producto: cambia el tamaño, el comportamiento de las banderas y la superficie de licencias | | `waterfox` | segundo navegador Gecko además de `atuq`. ¿Se shipean los dos? | | `valkey`, `opensmtpd`, `seaweedfs`, `step-ca`, `qdrant` | el perfil `servidor` hoy sirve web y SSH. Añadir base de datos, MTA, almacenamiento o CA es definir **qué clase de servidor es** | | `ia-modelo-embeddings` | **la mitad semántica del §6.3 de `atuq` no viaja.** `llama-cpp` y `ia-modelo-chat` sí están en los cuatro escritorios; el modelo de embeddings, no ⇒ hay motor y hay chat, y no hay búsqueda por significado | | `steam-runtime-sniper` | es una imagen **qorpa**, no un paquete: entra por el ADR 0015 | | `xorriso`, `mtools` | hacen medios de instalación. Si el instalador los necesita en la imagen, van; si los usa el hub, no. Hay que mirar el instalador antes de decidir | ### F · Catálogo, no imagen (30) `amfora`, `anew`, `bom`, `cargo-audit`, `cargo-edit`, `cargo-hack`, `chartmuseum`, `cheat`, `dnsx`, `flyctl`, `fortio`, `git-absorb`, `glab`, `goldilocks`, `gomi`, `krew`, `kubectl`, `kubescape`, `melange`, `nancy`, `pint`, `rekor-cli`, `revive`, `sake`, `scorecard`, `temporal-cli`, `timetrace`, `topgrade`, `xsv`, `yamlfmt`. Se construyen, se sellan y se instalan por nombre. No engordan ninguna imagen. **Lo mismo vale para las 529 del volcado crudo**: es un catálogo, y un catálogo grande es una virtud siempre que nadie lo confunda con la imagen. ⚠ Una de ellas merece una nota: **`topgrade` actualiza el sistema llamando a gestores de paquetes ajenos.** En una distro declarativa eso no es una comodidad, es un pie de cabra. Que esté en el catálogo está bien; que entre en una imagen, no. ## Lo que el barrido destapó, que era el punto ### 1. La imagen no podía conectarse a un WiFi `wpa_supplicant` estaba **sellado y en cero perfiles**. `dhcpcd` —que sí estaba en `base`— resuelve la IP de un cable; el handshake WPA2 de 4 vías lo hace un supplicant en userspace y **busybox no lo trae**. La receta llevaba meses escrita, con una cabecera que dice literalmente *«para asociar el WiFi del medio live al AP»*, y nadie la había declarado. Es el modo de fallo de esta casa en estado puro: **nada falla**. La imagen arranca, la métrica da N/N, `ip link` ve la interfaz — y no hay forma de asociarse. Sólo se ve usándolo, o cruzando dos ficheros que nadie cruzaba. ⚠ **Y esto NO cierra la red inalámbrica, sólo la destraba.** `networkmanager` existe **sólo en `incoming-kde`**: GNOME, COSMIC y sway **no lo alcanzan** (cola hermana; una receta resuelve sibling-first y después el catálogo padre, nunca una cola de al lado). O sea que en tres de los cuatro escritorios la WiFi se configura a mano con `wpa_cli`, y el indicador de red del escritorio no tiene con qué hablar. Promover `networkmanager` al corpus es su propia unidad de trabajo. ### 2. Tres de los cuatro escritorios no arrancaban NADA `paquetes` dice qué se **instala**; `servicios` dice qué se **levanta**. Son dos hechos distintos y sólo `escritorio-gnome` tenía el segundo. KDE, COSMIC y sway salían con `dbus`, `pipewire`, `upower` —y desde ayer `cupsd` y `bluetoothd`— **instalados y apagados**, con la métrica de clausura dando N/N igual. Las tres listas se escribieron computando qué ofrece cada clausura: se cruzaron los `[[service]]` de las **11 recetas que los declaran** (`accountsservice`, `arje-logind-compat`, `arje-polkit-compat`, `bluez`, `colord`, `cups`, `dbus`, `openssh`, `pipewire`, `upower`, `wireplumber`) contra el campo `perfiles` de los cinco grafos. **Contra la clausura y no contra las raíces**: `scripts/targets.py` imprime raíces, y la mitad de los demonios llegan como dependencia — mirar la lista equivocada dejaba fuera a `pipewire` y `colord` en GNOME, que sí están en la imagen. ### 3. Y ese mismo cruce destapó tres ausencias de verdad Ninguna se arregla declarando servicios; quedan escritas en `targets.toml` donde toca: | perfil | ausencia | consecuencia | |---|---|---| | **KDE**, **sway** | no hay `wireplumber` | `pipewire` sin gestor de sesión **no encamina nada**: los nodos existen y nadie los conecta | | **COSMIC** | no hay `upowerd` | dibuja un indicador de batería que no tiene de dónde leerla | | **KDE**, **sway** | no hay `logind-compat` | hoy lo cubren `seatd` y el propio compositor, pero es una suplencia, no un diseño | ## Cómo repetirlo ```sh # 1. hojas sin perfil, por clase python3 - <<'PY' import json, collections d=json.load(open('docs/state/build-state.json')); n=d['nodes'] hojas=[k for k,v in n.items() if not v.get('perfiles') and v.get('dependientes_total',0)==0] print(len(hojas), collections.Counter(n[k]['cls'] for k in hojas)) PY # 2. partir crudas / trabajadas por la marca del importador grep -L 'PUNTO DE PARTIDA' recipes/*.toml # 3. servicios que ofrece la clausura de un perfil (NO las raíces) # cruzar los [[service]] de recipes/**.toml con el campo `perfiles` de build-state*.json ``` **La regla que sale de todo esto**, y es la que conviene recordar cuando el recuento vuelva a subir: > Una receta sellada y no declarada **no es deuda**: es catálogo. Se vuelve deuda el día que lo que > falta es una capacidad — y eso no lo dice ninguna métrica de build, porque las métricas miden lo > declarado. Hay que preguntarle a la imagen qué **no puede hacer**, no al grafo qué le falta.