docs: el barrido de perfiles y servicios, con su método y la política que faltaba escribir

Deja escrito lo que el barrido midió y, sobre todo, CÓMO se midió, para que el recuento se pueda
repetir en vez de re-derivar.

Lo que aporta que no estaba en ningún lado:

1. **La política «catálogo ≠ imagen».** El repo ya la aplicaba de hecho y no la había escrito, y sin
   ella «620 hojas sin perfil» se lee como deuda. No lo es: 529 son volcado crudo del importador y
   355 son herramientas Go de nube. Una receta sellada y no declarada es CATÁLOGO — se instala por
   nombre. Se vuelve deuda sólo cuando lo que falta es una CAPACIDAD.

2. **El método en tres preguntas encadenadas**, y por qué el orden importa: ¿está en algún perfil?
   → ¿podría llegar por clausura ajena (`dependientes_total>0`)? → ¿fue curada alguna vez (la
   cabecera «PUNTO DE PARTIDA» del importador)? Las hojas son el hueco; las librerías no.

3. **El triaje completo de las 91 hojas trabajadas a mano**, en seis clases que suman 91 exactas.
   Incluye las que NO se declaran de oficio y por qué: `openrc` compite con arje-zero, `uutils` y
   coreutils/grep/findutils compiten con los applets de busybox, `waterfox` es un segundo Gecko,
   y valkey/opensmtpd/step-ca/qdrant definen QUÉ CLASE de servidor es el perfil `servidor`.

4. **Dos hallazgos que no eran el objetivo del barrido:**
   - `networkmanager` existe SÓLO en incoming-kde ⇒ GNOME, COSMIC y sway no lo alcanzan. Con
     wpa_supplicant la WiFi se destraba, pero se configura a mano y el indicador de red del
     escritorio no tiene con qué hablar.
   - `ia-modelo-embeddings` no está en ningún perfil: hay motor (llama-cpp) y hay chat
     (ia-modelo-chat) en los cuatro escritorios, y NO hay búsqueda por significado. La mitad
     semántica del §6.3 de atuq no viaja.

5. **Una trampa del instrumento**, que casi hace escribir mal las listas de servicios:
   `scripts/targets.py <perfil>` imprime las RAÍCES, no la clausura. Cruzar los [[service]] contra
   esa salida dejaba fuera a pipewire y colord en GNOME, que sí están en la imagen. La clausura la
   da el campo `perfiles` de los build-state*.json.
This commit is contained in:
Sergio
2026-09-13 01:08:17 +00:00
parent 9ac4e1727f
commit cbecd0ba12
+202
View File
@@ -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 <nombre>` 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.