tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ 6995dbb44eb733794d29bc8298cba3ac465ac783 docs/plan-barrido-perfiles-y-servicios.md Markdown · 202 líneas · 11.5 KB Historia crudo

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

1# Plan — barrido de perfiles y servicios, MEDIDO
2
3**Fecha:** 2026-09-13 · **Catálogo contra el que se midió:** 905 nodos del corpus (903 recetas
4selladas + 2 ajenos) y los cuatro grafos de cola · **Instrumento:** el campo `perfiles` de
5`docs/state/build-state*.json` cruzado con `dependientes_total`, más el `[[service]]` de las
6recetas. No se contó nada a mano.
7
8## Por qué existe este documento
9
10Salió de una pregunta lateral. Al declarar doce recetas nuevas en `targets.toml` hubo que mirar qué
11había ya declarado, y el número no se parecía a lo que cualquiera habría dicho de memoria:
12
13> **De 905 nodos, 631 no estaban en NINGÚN perfil.** El 73 % del catálogo estaba sellado, íntegro,
14> reproducible… y no viajaba en ninguna imagen.
15
16Lo grave no es el número: es que **nada lo decía**. `drenaje.json` daba deuda 0 —y era cierto, no
17hay nada roto—. La métrica de perfil daba N/N —y también era cierta, porque mide la clausura de lo
18**declarado** y no puede ver lo que falta en la declaración. Es la lección de `foot` (un perfil
19121/121 sin emulador de terminal) aplicada al catálogo entero, y llevaba meses ahí.
20
21## El método, para poder repetirlo
22
23Tres preguntas encadenadas, y el orden importa porque cada una descarta ruido de la siguiente:
24
251. **¿Está en algún perfil?** → campo `perfiles` de los cinco `build-state*.json`. Hay que mirar los
26   cinco: una receta del corpus puede estar declarada desde una cola.
272. **¿Podría llegar por la clausura de otra?** → `dependientes_total > 0`. Una librería sin perfil
28   NO es un hueco: llega como dep de quien la usa. **El hueco son las HOJAS**: si nadie depende de
29   ellas y ningún perfil las declara, no hay camino por el que entren a una imagen.
303. **¿Fue curada alguna vez?** → la cabecera del importador. Las recetas de `import-nix` /
31   `import-alpine` traen literalmente «PUNTO DE PARTIDA, no final». Es una marca objetiva y parte
32   el montón en dos mitades con historias distintas.
33
34```
35905 nodos
36├── 274 en algún perfil
37└── 631 sin perfil
38    ├──  11 con dependientes  → llegan por clausura ajena, NO son hueco
39    └── 620 HOJAS             → sin camino a ninguna imagen
40        ├── 529 volcado crudo del importador  (328 Go · 195 Rust · 6 C)
41        └──  91 trabajadas a mano             ← éstas se triaron una por una
42```
43
44## La política que faltaba escribir: **catálogo ≠ imagen**
45
46La tentación al ver «620 hojas sin perfil» es declararlas todas. Sería peor que no haber mirado: 355
47de ellas son herramientas Go de nube y desarrollo importadas en tanda, y meter `kubectl`, `flyctl` y
48`chartmuseum` en la imagen de escritorio de alguien no arregla nada — engorda cuatro imágenes para
49servir a nadie.
50
51Lo que faltaba no era declarar: era **decir en voz alta que el catálogo tiene dos niveles**, cosa
52que el repo ya hacía de hecho y no había escrito en ningún lado.
53
54| nivel | qué es | cómo llega a una máquina |
55|---|---|---|
56| **imagen** | lo que un perfil declara, más su clausura | viene horneado; es lo que arranca |
57| **catálogo** | todo lo demás que el repo sabe construir y tiene sellado | `takana install <nombre>` desde el repo |
58
59⇒ **No estar en un perfil no es deuda.** Es deuda sólo cuando lo que falta es una **capacidad**: algo
60que la imagen, tal como está, no puede hacer. Ese es el criterio con el que se triaron las 91, y es
61el que hay que aplicar la próxima vez.
62
63## El triaje de las 91, completo
64
65Las seis clases suman 91 exactas; ninguna receta quedó sin clasificar.
66
67### A · Declaradas en este barrido (9)
68
69| receta | dónde | por qué |
70|---|---|---|
71| **`wpa_supplicant`** | `base` | **la imagen no podía asociarse a un WiFi** — ver abajo |
72| `wlr-randr` | `escritorio-sway` | no había con qué cambiar resolución ni rotar una pantalla |
73| `nano` | `cli` | `base` sólo trae `vim` |
74| `tig` | `cli` | git en TUI |
75| `patch` | `cli` | aplicar un diff a mano; userland clásico que faltaba |
76| `socat` | `cli` | sockets y diagnóstico de red |
77| `pigz` | `cli` | gzip en paralelo |
78| `miller` | `cli` | `jq` para CSV/TSV — el hueco de datos tabulares |
79| `dwarves` | `cli` | `pahole`: esta distro compila su propio kernel |
80
81### B · Del proyecto — entran por otro camino, no por perfil (19)
82
83`arje-zero`, `arje-absorb`, `arje-installer`, `arje-packager`, `arje-getty-stub`,
84`arje-net-bring-up`, `arje-zero-attest`, `hammerd`, `hammer-edit`, `agora-cli`, `dominium-cli`,
85`cosmos-cli`, `tinkuy-sim`, `llimphi-counter`, y los cinco kernels (`linux`, `linux-generic`,
86`linux-metal`, `linux-metal-dual`, `linux-metal-kexec`).
87
88El init y el kernel los hornea el `product-rootfs`; los binarios de la suite viven en tawasuyu. Que
89estén fuera de los perfiles es correcto — **pero conviene que esté escrito**, porque «`arje-zero` no
90está en ningún perfil» se lee como alarma hasta que alguien explica por qué no lo estará nunca.
91
92### C · Andamios y pruebas (6)
93
94`adwaita-hello`, `gtk4-hello`, `sourceview-hello`, `poppler-render-check`, `firefox-instrumentado`,
95`mesa-llvmpipe`. Existen para probar una cadena o para alimentar un PGO. No van a una imagen y no
96deberían aparecer nunca en este recuento como deuda.
97
98### D · Librerías sin consumidor todavía (10)
99
100`brotli`, `libarchive`, `libsodium`, `libssh2`, `libuv`, `lz4`, `zlib-ng`, `musl`, `bwrap`, `crun`.
101
102⚠ **Éstas son las únicas de las 620 que merecen una segunda mirada por sí mismas**, y no por falta de
103perfil: son librerías con **cero dependientes**. Alguien las escribió para algo y ese algo no llegó,
104o llegó y resolvió contra otra. Un `libuv` sellado que nadie usa es trabajo hecho y no cobrado —
105`libuv` es, por ejemplo, dep de `neovim`, que el catálogo no tiene.
106
107### E · Decisión abierta — NO se declaran de oficio (17)
108
109Cada una compite con algo ya elegido, o enciende una función que nadie pidió. Declararlas por
110reflejo sería tomar una decisión de distro dentro de un barrido de limpieza:
111
112| receta(s) | con qué compite / qué decide |
113|---|---|
114| `openrc` | **compite con `arje-zero`**: los inits no se apilan |
115| `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 |
116| `waterfox` | segundo navegador Gecko además de `atuq`. ¿Se shipean los dos? |
117| `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** |
118| `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 |
119| `steam-runtime-sniper` | es una imagen **qorpa**, no un paquete: entra por el ADR 0015 |
120| `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 |
121
122### F · Catálogo, no imagen (30)
123
124`amfora`, `anew`, `bom`, `cargo-audit`, `cargo-edit`, `cargo-hack`, `chartmuseum`, `cheat`, `dnsx`,
125`flyctl`, `fortio`, `git-absorb`, `glab`, `goldilocks`, `gomi`, `krew`, `kubectl`, `kubescape`,
126`melange`, `nancy`, `pint`, `rekor-cli`, `revive`, `sake`, `scorecard`, `temporal-cli`, `timetrace`,
127`topgrade`, `xsv`, `yamlfmt`.
128
129Se construyen, se sellan y se instalan por nombre. No engordan ninguna imagen. **Lo mismo vale para
130las 529 del volcado crudo**: es un catálogo, y un catálogo grande es una virtud siempre que nadie lo
131confunda con la imagen.
132
133⚠ Una de ellas merece una nota: **`topgrade` actualiza el sistema llamando a gestores de paquetes
134ajenos.** En una distro declarativa eso no es una comodidad, es un pie de cabra. Que esté en el
135catálogo está bien; que entre en una imagen, no.
136
137## Lo que el barrido destapó, que era el punto
138
139### 1. La imagen no podía conectarse a un WiFi
140
141`wpa_supplicant` estaba **sellado y en cero perfiles**. `dhcpcd` —que sí estaba en `base`— resuelve
142la IP de un cable; el handshake WPA2 de 4 vías lo hace un supplicant en userspace y **busybox no lo
143trae**. La receta llevaba meses escrita, con una cabecera que dice literalmente *«para asociar el
144WiFi del medio live al AP»*, y nadie la había declarado.
145
146Es el modo de fallo de esta casa en estado puro: **nada falla**. La imagen arranca, la métrica da
147N/N, `ip link` ve la interfaz — y no hay forma de asociarse. Sólo se ve usándolo, o cruzando dos
148ficheros que nadie cruzaba.
149
150⚠ **Y esto NO cierra la red inalámbrica, sólo la destraba.** `networkmanager` existe **sólo en
151`incoming-kde`**: GNOME, COSMIC y sway **no lo alcanzan** (cola hermana; una receta resuelve
152sibling-first y después el catálogo padre, nunca una cola de al lado). O sea que en tres de los
153cuatro escritorios la WiFi se configura a mano con `wpa_cli`, y el indicador de red del escritorio
154no tiene con qué hablar. Promover `networkmanager` al corpus es su propia unidad de trabajo.
155
156### 2. Tres de los cuatro escritorios no arrancaban NADA
157
158`paquetes` dice qué se **instala**; `servicios` dice qué se **levanta**. Son dos hechos distintos y
159sólo `escritorio-gnome` tenía el segundo. KDE, COSMIC y sway salían con `dbus`, `pipewire`, `upower`
160—y desde ayer `cupsd` y `bluetoothd`— **instalados y apagados**, con la métrica de clausura dando
161N/N igual.
162
163Las tres listas se escribieron computando qué ofrece cada clausura: se cruzaron los `[[service]]` de
164las **11 recetas que los declaran** (`accountsservice`, `arje-logind-compat`, `arje-polkit-compat`,
165`bluez`, `colord`, `cups`, `dbus`, `openssh`, `pipewire`, `upower`, `wireplumber`) contra el campo
166`perfiles` de los cinco grafos. **Contra la clausura y no contra las raíces**: `scripts/targets.py`
167imprime raíces, y la mitad de los demonios llegan como dependencia — mirar la lista equivocada dejaba
168fuera a `pipewire` y `colord` en GNOME, que sí están en la imagen.
169
170### 3. Y ese mismo cruce destapó tres ausencias de verdad
171
172Ninguna se arregla declarando servicios; quedan escritas en `targets.toml` donde toca:
173
174| perfil | ausencia | consecuencia |
175|---|---|---|
176| **KDE**, **sway** | no hay `wireplumber` | `pipewire` sin gestor de sesión **no encamina nada**: los nodos existen y nadie los conecta |
177| **COSMIC** | no hay `upowerd` | dibuja un indicador de batería que no tiene de dónde leerla |
178| **KDE**, **sway** | no hay `logind-compat` | hoy lo cubren `seatd` y el propio compositor, pero es una suplencia, no un diseño |
179
180## Cómo repetirlo
181
182```sh
183# 1. hojas sin perfil, por clase
184python3 - <<'PY'
185import json, collections
186d=json.load(open('docs/state/build-state.json')); n=d['nodes']
187hojas=[k for k,v in n.items() if not v.get('perfiles') and v.get('dependientes_total',0)==0]
188print(len(hojas), collections.Counter(n[k]['cls'] for k in hojas))
189PY
190
191# 2. partir crudas / trabajadas por la marca del importador
192grep -L 'PUNTO DE PARTIDA' recipes/*.toml
193
194# 3. servicios que ofrece la clausura de un perfil (NO las raíces)
195#    cruzar los [[service]] de recipes/**.toml con el campo `perfiles` de build-state*.json
196```
197
198**La regla que sale de todo esto**, y es la que conviene recordar cuando el recuento vuelva a subir:
199
200> Una receta sellada y no declarada **no es deuda**: es catálogo. Se vuelve deuda el día que lo que
201> falta es una capacidad — y eso no lo dice ninguna métrica de build, porque las métricas miden lo
202> declarado. Hay que preguntarle a la imagen qué **no puede hacer**, no al grafo qué le falta.

plan-barrido-perfiles-y-servicios.md

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.

nivelqué escómo llega a una máquina
imagenlo que un perfil declara, más su clausuraviene horneado; es lo que arranca
catálogotodo lo demás que el repo sabe construir y tiene selladotakana 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)

recetadóndepor qué
wpa_supplicantbasela imagen no podía asociarse a un WiFi — ver abajo
wlr-randrescritorio-swayno había con qué cambiar resolución ni rotar una pantalla
nanoclibase sólo trae vim
tigcligit en TUI
patchcliaplicar un diff a mano; userland clásico que faltaba
socatclisockets y diagnóstico de red
pigzcligzip en paralelo
millerclijq para CSV/TSV — el hueco de datos tabulares
dwarvesclipahole: 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
openrccompite con arje-zero: los inits no se apilan
uutils, coreutils, diffutils, findutils, findutils-xargs, grepcompiten 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
waterfoxsegundo navegador Gecko además de atuq. ¿Se shipean los dos?
valkey, opensmtpd, seaweedfs, step-ca, qdrantel 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-embeddingsla 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-sniperes una imagen qorpa, no un paquete: entra por el ADR 0015
xorriso, mtoolshacen 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:

perfilausenciaconsecuencia
KDE, swayno hay wireplumberpipewire sin gestor de sesión no encamina nada: los nodos existen y nadie los conecta
COSMICno hay upowerddibuja un indicador de batería que no tiene de dónde leerla
KDE, swayno hay logind-compathoy lo cubren seatd y el propio compositor, pero es una suplencia, no un diseño

Cómo repetirlo

# 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.