diff --git a/docs/plan-barrido-perfiles-y-servicios.md b/docs/plan-barrido-perfiles-y-servicios.md new file mode 100644 index 00000000..6b36741b --- /dev/null +++ b/docs/plan-barrido-perfiles-y-servicios.md @@ -0,0 +1,202 @@ +# 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.