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 1472Shell 176Rust 106Markdown 953592 Definitions

⎇ 6c207d3092c8cac0a5ad137e5d272d3efd9777ef docs/22-configurador-kernel.md Markdown · 511 lines · 29.8 KB History raw

takana etapa 5b: los 59 docs de diseño, runbooks y ADR 97ceb7241 · Sergio ·

1# SDD 22 — Configurador de kernel por niveles: respuesta al handoff de tawasuyu
2
3Escrito 2026-08-07, en respuesta a `tawasuyu/HANDOFF-KERNEL-CONFIG-A-HAMMER.md`.
4
5El handoff avisa de su propio método: *«nada de lo que sigue está verificado contra el código de
6takana — si un dato de takana lo contradice, manda takana»*. Este documento es esa verificación. **No
7es una aprobación ni un plan de implementación**: contesta las seis preguntas de su §9 con evidencia,
8corrige lo que estaba mal, y señala dos objeciones técnicas que cambian el diseño.
9
10**Veredicto corto:** la idea se sostiene y el sustrato está más listo de lo que el handoff supone —
11el mecanismo de §6 (*«la app nunca escribe un `.config`»*) **ya es exactamente como funciona hoy**.
12Pero hay **un hecho estructural que el handoff no podía conocer y que reordena todo**, y **una
13objeción técnica a §2.1** que hay que resolver antes de escribir una línea.
14
15---
16
17## 1. El hecho que reordena todo: **el config ES la identidad del artefacto**
18
19En takana, `Recipe::hash_inputs` es una lista blanca, y **las fases de build entran en ella**. El
20config del kernel vive dentro de la fase `configure` de `recipes/linux*.toml`. Por lo tanto:
21
22> **Cambiar un solo símbolo de config cambia el `ArtifactHash` del kernel.**
23
24Esto no es un detalle de implementación: es la bisagra del diseño, y corta en los dos sentidos.
25
26**A favor, y fuerte:** §2.2 (config atestado por huella de hardware) **sale casi gratis**. No hay que
27inventar un mecanismo para «atestar un config»: cada config ya *es* un artefacto direccionado por
28contenido, reproducible bit a bit y firmable con la maquinaria que existe. Un «config que booteó en
29esta huella» es, literalmente, un `ArtifactHash` + una firma. El handoff intuyó que takana tenía el
30sustrato; lo tiene más de lo que creía.
31
32**En contra, y también fuerte:** la explosión de builds que §2.2 quiere evitar **no es hipotética,
33es aritmética visible en el disco**. Cada variante de config es un artefacto nuevo en el store, y el
34store ya crece 24 G por ciclo de trabajo sólo con las variantes que producimos hoy sin querer. Con
35N1 (~40–60 bundles) el espacio combinatorio es 2⁴⁰; con «perfiles publicados» —que es lo que el
36handoff propone en §8— es un puñado. **La restricción de §8 no es una preferencia de diseño: es una
37condición de supervivencia del disco**, y conviene decirlo así en la UI.
38
39**Consecuencia práctica que hay que aceptar desde el día uno:** una «perilla» de la UI **no es un
40parámetro de runtime, es una edición de receta**. No hay forma de tener un config variable sin
41generar recetas. El diseño correcto es que `takana kernel plan` **emita una receta derivada** (con su
42propio hash) en vez de fingir que el kernel es un binario parametrizable.
43
44---
45
46## 2. Las seis preguntas del §9, contestadas con evidencia
47
48### P1 — ¿Las recetas de kernel aceptan fragmentos o sólo defconfig completo?
49**Ya son fragmentos, y con la forma que §6 prescribe.** `recipes/linux.toml`, fase `configure`:
50
51```
52make ARCH=x86_64 defconfig && \
53scripts/config -d MODULE_SIG -d DEBUG_INFO_BTF -e SECURITY_LANDLOCK -e AUDIT \
54  -d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL  … && \
55make ARCH=x86_64 olddefconfig
56```
57
58Base + deltas por símbolo + resolución por el `olddefconfig` **del propio kernel**. La app nunca
59escribe un `.config`. La única diferencia con el handoff es el vehículo: `scripts/config -e/-d` en
60vez de ficheros de fragmento y `merge_config.sh`. Son equivalentes y `scripts/config` es más fácil de
61generar programáticamente. **Media implementación de §6 ya está en producción.**
62
63Falta la otra mitad, que es la que da la UI veraz: **el diff-back**. Hoy nada comprueba que los
64símbolos pedidos hayan sobrevivido a `olddefconfig`. Un `-e FOO` cuya dependencia no se cumple se
65pierde en silencio — el mismo patrón de fallo que el `license` que caía dentro de `[deps]`.
66
67### P2 — ¿El build es bit-reproducible con config variable?
68**Sí, y por construcción.** El config no es un parámetro externo: es parte de la entrada hasheada
69(§1). Cada config es su propia receta con su propio hash y su propia garantía de reproducción. No hay
70un «kernel» cuya reproducibilidad dependa de congelar el config; hay N kernels, cada uno congelado.
71⇒ **§2.2 puede prefirmar artefactos, no sólo configs.**
72
73### P3 — ¿Cuánto tarda un kernel completo? 🚨
74Declarado en las propias recetas: **`linux` ~35 min, `linux-metal` ~45 min, `linux-generic` ~60 min**
75en la máquina de referencia. En la más lenta del parque, más.
76
77**Esto mata la bisección ingenua de §5.** ~6 rebuilds × 45 min = **4,5 horas** de la máquina del
78usuario para encontrar un bundle culpable. Nadie espera eso, y menos con la máquina que acaba de no
79arrancar. El handoff ya preveía el riesgo («si es caro, tope duro de iteraciones»); la respuesta es
80que **es caro**. Ver §4, donde propongo una bisección que no recompila.
81
82### P4 — ¿harkaq puede prestar su motor de clausura?
83**El patrón sí; el código no.** `crates/hammer-build/src/harkaq.rs` deriva su política enumerando
84**fichero a fichero** los artefactos de las deps declaradas — es una clausura sobre el grafo de
85paquetes materializado en el store, con semántica de rutas de filesystem. Kconfig es otro grafo, con
86otra semántica y ~15 000 símbolos. Reusar la *idea* («política = clausura, no lista») es correcto y
87está validado; reusar la *implementación* no. Hace falta un lector de Kconfig — y ahí está la
88objeción del §3.
89
90### P5 — ¿El índice firmado admite un eje más (la huella)?
91**Sí, y sin romper nada.** `PackageEntry` (`crates/hammer-core/src/repo.rs`) es un struct serde cuyos
92campos opcionales llevan `#[serde(default, skip_serializing_if = ...)]`. Añadir `fingerprint:
93Option<String>` es compatible hacia atrás: los índices viejos siguen parseando y los clientes viejos
94ignoran el campo. **Es exactamente el truco que hizo pagable el campo `license` hoy mismo.**
95
96### P6 — El prerrequisito de PLAN-KIKIN §4.bis
97**Estaba dado por pendiente y está hecho en dos de tres.** Medido:
98
99| receta | `SECURITY_LANDLOCK` | `AUDIT` | `IO_URING` | `BPF_SYSCALL` |
100|---|---|---|---|---|
101| `linux-metal` | ✅ `-e` | ✅ `-e` | ✅ `-e` | ✅ `-e` |
102| `linux-generic` | ✅ `-e` | ✅ `-e` | ✅ `-e` | ✅ `-e` |
103| `linux` | ✅ `-e` | ✅ `-e` | ❌ sólo en un comentario | ❌ sólo en un comentario |
104
105⚠️ Y la forma de medirlo importa: `grep IO_URING recipes/linux.toml` **da positivo** porque la palabra
106aparece en un comentario. Hay que mirar los `-e`/`-d` **dentro del bloque `configure`**. Es el mismo
107error que hoy infló el conteo de licencias (`grep -l license` contaba comentarios y nombres de
108paquete) y el que hace mentir a la regla del `.a` no-PIC. **En este repo, `grep` sobre ficheros con
109comentarios densos no es una medición.**
110
111⇒ Queda pendiente sólo `linux`, que es el kernel de QEMU/serial.
112
113---
114
115## 3. Objeción técnica a §2.1: «clausura» sobre Kconfig no está bien definida todavía
116
117La propuesta —«todo lo alcanzable desde `CONFIG_WIRELESS` menos lo alcanzable desde lo que sí
118quiero»— asume un grafo con aristas de un solo tipo. Kconfig tiene **al menos tres, con semánticas
119incompatibles**:
120
121- `depends on` — arista hacia arriba: *no puedo existir sin X*.
122- `select` — arista hacia abajo y **forzada**: *si yo entro, X entra*, ignorando los `depends on` de
123  X. Es la fuente clásica de configs inválidos.
124- `imply` — sugerencia blanda.
125
126«Alcanzable» por `depends on` y «alcanzable» por `select` dan conjuntos distintos y **ninguno de los
127dos es la respuesta**. Un driver de otro subsistema puede `select` un helper de wireless sin ser
128wireless: cae dentro de la clausura ingenua y apagarlo rompe algo que el usuario sí quería. Y al
129revés, `select` puede meter símbolos wireless desde fuera de la clausura.
130
131**Esto no invalida §2.1 — lo convierte en la parte con contenido técnico real**, tal como el propio
132handoff dice en su §10. Pero hay que definir la semántica de arista antes de escribir el predicado, y
133la definición hay que **validarla contra algo**.
134
135### Y tenemos contra qué validarla, hoy y gratis
136El config actual de `linux.toml` **ya es un juego de bundles N1 escrito a mano**. Literalmente:
137
138```
139-d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL      ← el bundle «no necesito wifi»
140-d SOUND -d SND                                            ← «no necesito audio»
141-d DRM -d DRM_I915 -d DRM_AMDGPU -d DRM_NOUVEAU -d FB      ← «sin gráficos»
142-d XFS_FS -d BTRFS_FS -d F2FS_FS -d JFS_FS -d REISERFS_FS  ← «sólo ext4»
143```
144
145Es un corpus de verdad-de-campo: alguien decidió a mano qué símbolos componen «no necesito wifi», y
146ese kernel **arranca**. La prueba de §2.1 es directa y barata: *¿la clausura calculada de
147`CONFIG_WIRELESS` reproduce ese conjunto?* Si sobra o falta mucho, la semántica de arista está mal y
148nos enteramos en una tarde, sin compilar nada. **Refuerza el §10 del handoff: eso se hace antes que
149nada, junto al modo reversa.**
150
151---
152
153## 4. Lo que agrego: bisección **sin recompilar**
154
155Dado P3 (35–60 min por kernel), la bisección de §5 cuesta horas. Pero la mayoría de los bundles N1
156son **drivers**, y un driver puede ser `=m` en vez de `=y`.
157
158Propuesta: **un kernel «superset» con todo lo bisecable como módulo**. Los bundles N1 de tipo
159hardware no se resuelven entonces recompilando, sino con una **lista de bloqueo de módulos en la
160línea de arranque**. La bisección deja de ser 6 rebuilds (4,5 h) y pasa a ser **6 reinicios**
161(minutos), sobre un kernel que ya está en el árbol de generaciones.
162
163Consecuencias:
164- El «kernel a medida» pasa a ser una optimización de tamaño/arranque, no un requisito de
165  funcionalidad ⇒ el usuario puede probar bundles antes de pagar un build.
166- Encaja con el árbol de generaciones y el rollback E4, que ya existen.
167- **Limitación honesta:** no cubre lo que debe ser `=y` (LSM, preempción, unwinder, `-march`, LTO,
168  o sea buena parte de N2). Para esos sí hay rebuild y sí hay que topar iteraciones.
169- **Y un coste que hay que decir:** el superset es exactamente el kernel gordo que el nivel 1 quiere
170  evitar. Es una herramienta de diagnóstico, no el kernel de todos los días.
171
172## 5. Lo que agrego: el gate de no-regresión (§6) debe ser **por objetivo**, no global
173
174El handoff lo llama la mejor relación valor/esfuerzo y estoy de acuerdo. Pero el config actual de
175`linux.toml` apaga `USB_SUPPORT`, `HID`, `HID_SUPPORT`, `INPUT_MOUSE`, `INPUT_TOUCHSCREEN`. Ese
176kernel **no puede recibir teclado en metal** — y es correcto, porque es el kernel de QEMU con consola
177serie. Un gate global «todo dispositivo en uso debe seguir teniendo driver» **rechazaría una receta
178sana**.
179
180⇒ El gate necesita el objetivo declarado (`qemu-serial` / `metal` / `servidor`) y una noción de
181«dispositivos en uso **de la máquina destino**», que no siempre es la máquina donde se construye.
182Sigue siendo la propuesta más barata de las nueve; sólo que el portón se pone en el sitio correcto.
183
184## 6. Dos huecos del handoff que conviene cerrar antes
185
186**«Booteó» no es una aserción suficiente para atestar (§2.2).** Un kernel puede arrancar y dejarte
187sin red, sin wifi o sin teclado — que es justo el desastre que §6 quiere evitar. Si la atestación
188dice sólo «booteó en esta huella», estamos firmando la clase de fallo que más duele. La atestación
189debería llevar **qué se comprobó**: qué dispositivos de la huella quedaron con driver bindeado
190después de arrancar. Es el mismo dato que ya calcula el gate de §6 ⇒ una implementación, dos usos.
191
192**El catálogo de bundles necesita las tres etiquetas de nivel, no dos.** El handoff avisa en §5 que
193«la mitad de N2 no es Kconfig sino variables de receta». Con §1 en la mano eso es más grave de lo que
194parece: las perillas de receta (LTO, `-march`, `zig_version`) **cambian el hash igual que los
195símbolos**, pero se aplican en otra fase y fallan de otra forma. La UI tiene que saber de qué lado
196cae cada perilla o prometerá diffs que no puede explicar.
197
198---
199
200## 7. Qué contesto al reparto (§7)
201
202De acuerdo con el reparto tal como está. Dos precisiones desde este lado:
203
204- El contrato JSON: de acuerdo, y el precedente que cita (`/run/hammer/boot-graph.json`) es el
205  correcto. Añadiría que el JSON de `takana kernel plan` **debe incluir el `ArtifactHash` resultante**,
206  porque por §1 el plan *determina* el artefacto: la UI puede decir «esto ya está construido y
207  firmado» sin construir nada.
208- Los verbos propuestos me parecen bien salvo `takana kernel build --plan`, que esconde dos cosas muy
209  distintas (construir, y dar de alta en el árbol de generaciones). Separarlos deja que la sonda en
210  VM de §4 se meta entre medio como un paso propio y auditable.
211
212## 8. Orden que propongo (modifica el §10 del handoff)
213
2141. **Modo reversa (#9)** — tal cual lo propone: cero riesgo, valida el catálogo contra un kernel real.
2152. **Validar la clausura contra el config actual** (§3 de este documento) — sale gratis, usa un corpus
216   que ya existe, y decide si §2.1 es viable. **Va antes que el gate**, porque si la clausura no
217   reproduce los bundles hechos a mano, el diseño de N1 cambia.
2183. **Gate de no-regresión (#6), por objetivo** (§5 de este documento).
2194. **Diff-back** (la mitad que falta de §6 del handoff): barato, y sin él la UI miente.
2205. Recién después, #1 (clausuras) con la semántica ya decidida.
221
222**No implementar todavía.** Este documento existe para que la decisión se tome con los seis datos
223verificados, no con las suposiciones del handoff — que su autor, correctamente, marcó como tales.
224
225---
226
227# ADENDA — 2026-08-10: implementación en marcha
228
229El usuario decidió. Lo que sigue ya no es análisis: es lo medido contra el código escrito. Se sigue
230el orden del §8.
231
232## 9. Paso 2 hecho: la clausura SÍ reproduce los bundles a mano
233
234`takana kernel` (`crates/hammer-core/src/kernel/`, `crates/hammer-cli/src/kernel_cmd.rs`) trae un
235lector de Kconfig **de sólo análisis** — la regla dura del §6 del handoff se respeta literalmente: no
236hay solver, el `.config` lo sigue produciendo `olddefconfig`.
237
238Salud del lector, sobre `linux-6.16.12` (`takana kernel stats`):
239
240| ficheros | símbolos | visibles | con ayuda | aristas `select` | aristas `imply` | avisos de parseo |
241|---|---|---|---|---|---|---|
242| 1646 | 18 212 | 15 169 | 14 866 | 15 165 | 445 | **0** |
243
244### La semántica de arista, resuelta
245El §3 pedía definirla antes de escribir el predicado. Quedó así, y las dos mitades importan:
246
247- **`depends on` en posición conjuntiva** — sólo cuenta como dependencia dura el símbolo que, puesto
248  a `n`, apaga la expresión entera. En `A && (B || C)` sólo `A` es duro; `!X`, las ramas de un `||` y
249  los dos lados de una comparación no aportan nada. Es deliberadamente conservador: preferimos un
250  bundle que apague **de menos** (se nota) a uno que apague de más (hace un ladrillo).
251- **Un símbolo con varias definiciones** muere sólo si mueren todas ⇒ las dependencias duras son la
252  **intersección entre definiciones**, no la unión. El error natural (la unión) diría que apagar `Y`
253  mata a `X` cuando `X` tiene otra definición que ni menciona a `Y`.
254- **`select` no es una arista más: es el portillo.** Fuerza el destino **ignorando** sus
255  `depends on`, así que un símbolo de fuera del bundle reenciende lo que el bundle apagó.
256  `select_leaks` las enumera, y `closure_off_fixpoint` cierra el bundle contra ellas — **reportando
257  el precio, nunca aplicándolo solo**.
258
259### La medición que decide §2.1
260Corpus: el juego de bundles N1 escrito a mano en la fase `configure` de `recipes/linux.toml`, sobre
261un kernel que arranca. Pregunta: *¿la clausura calculada desde la raíz mínima reproduce lo que
262escribió el humano?*
263
264```
265takana kernel closure --kconfig <árbol> WIRELESS --fixpoint
266```
267
268| | símbolos |
269|---|---|
270| clausura estricta de `WIRELESS` (sólo `depends on`) | 350 |
271| punto fijo (cierra las 3 fugas `select`: `WLAN`, `IWLEGACY`, `GELIC_WIRELESS`) | **406** |
272| bundle `-d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL` hecho a mano | **421** |
273| **sobra** (apagaría algo que el humano dejó vivo) | **0** |
274| **falta** | **15** |
275
276**Sobra cero.** La clausura no toca un solo símbolo que el humano quisiera conservar — que era el
277riesgo real, porque sobrar es hacer un ladrillo. Cierra en **una ronda**.
278
279Los 15 que faltan son **todos de `RFKILL`** (`RFKILL{,_GPIO,_INPUT,_LEDS}` y los drivers de laptop
280que dependen de él: `DELL_RBTN`, `IDEAPAD_LAPTOP`, `MSI_LAPTOP`, `AMILO_RFKILL`…). Y `RFKILL` **no
281es wifi**: es el interruptor de radio compartido, que bluetooth y NFC declaran con el mismo idioma
282(`depends on RFKILL || !RFKILL` en `net/{bluetooth,nfc,wireless}/Kconfig`). Que quede fuera no es un
283fallo de la clausura — es que el humano apagó **dos** bundles a la vez («no necesito wifi» y «no
284necesito radios») y los escribió en la misma línea. ⇒ **el catálogo N1 necesita «sin radios» como
285entrada propia.** Nótese que ese idioma es un `||`: la regla conservadora lo excluye de los duros,
286que es exactamente lo correcto.
287
288⇒ **§2.1 es viable y la objeción del §3 queda resuelta**, con una corrección al diseño: un bundle no
289es `clausura(raíz)` sino **`clausura(raíz)` + el punto fijo sobre `select`**, y las fugas se
290aprueban a mano una vez, no en cada release.
291
292### Y el punto fijo también dice cuándo NO
293No siempre cierra barato, y eso es información, no un fallo. Medido sobre los otros tres bundles a
294mano de `linux.toml`:
295
296| bundle a mano | clausura | fugas `select` | qué las causa |
297|---|---|---|---|
298| `WLAN WIRELESS CFG80211 MAC80211 RFKILL` | 421 | **0** | cerrado |
299| `SOUND SND` | 1323 | 19 sobre 7 destinos | `DRM_I915`/`DRM_NOUVEAU`/`DRM_AMD_DC` hacen `select` del códec HDMI |
300| `DRM DRM_I915 … FB AGP` | 770 | 15 sobre 6 destinos | drivers USB-C (`TYPEC_*`) hacen `select DRM_AUX_BRIDGE` |
301| `XFS_FS BTRFS_FS … NTFS3_FS` | 46 | 1 | `NTFS3_FS ← NTFS_FS` |
302
303Para cerrar «no necesito audio» hay que tragarse drivers gráficos. En `linux.toml` sale gratis
304porque los gráficos también están apagados, pero en un perfil de escritorio **no**: ahí la fuga es
305una decisión, y por eso el punto fijo la muestra en vez de aplicarla.
306
307## 10. Pasos 1 y 4 hechos: modo reversa, plan y diff-back
308
309**Modo reversa** (`takana kernel probe`) sobre gioser, con el catálogo `docs/state/kernel-bundles.toml`
310(15 bundles N1, 8 perillas N2): 10 551 símbolos declarados, 20 dispositivos PCI, 38 drivers
311bindeados, **7 bundles con capacidad que este hardware no usa**. Y avisa de lo que el propio caso
312destapó: el config vivo es de la serie 7.1 y el catálogo se revisó contra la 6.16 ⇒ las clausuras
313son aproximadas. Se dice en vez de callarlo.
314
315**Plan** (`takana kernel plan`): emite una **receta derivada**, como manda §1. Medido sobre
316`recipes/linux.toml` con `sin-wifi` + `sin-audio` + `solo-ext4` + `jaula-y-eio-moderna`:
317
318| | |
319|---|---|
320| banderas emitidas | **16** |
321| símbolos que caen con su clausura | **1775** |
322| `ArtifactHash` de la base | `b3:cb926743…` |
323| `ArtifactHash` de la derivada | `b3:47a52b2e…` |
324
325Dos hashes distintos para el mismo código fuente: **el config es la identidad**, y ahora se ve. El
326plan lleva su hash dentro ⇒ la UI puede decir «esto ya está construido y firmado» sin construir.
327
328Tres detalles del plan que no eran obvios:
329- **Se emiten raíces, no clausuras.** 16 banderas, no 1775 líneas. La clausura la calcula el
330  `olddefconfig` del propio kernel; takana la sabe sólo para poder explicarla.
331- **La fase derivada AÑADE una segunda ronda** (`… && scripts/config … && make olddefconfig`) en vez
332  de reescribir la base. No hay que parsear el shell de nadie y la base sigue siendo literalmente la
333  de siempre en el diff.
334- **Los conflictos se rechazan, no se ordenan.** Dos selecciones que se contradicen sobre un símbolo
335  darían una respuesta plausible y arbitraria según el orden de aparición. Y hay un segundo conflicto
336  que el símbolo solo no delata: encender algo que **cae dentro de la clausura** de lo que otro
337  bundle apaga — `olddefconfig` lo descartaría sin decir nada.
338
339**Diff-back** (`takana kernel diff-back --plan … --config …`): la mitad que faltaba del §6 del
340handoff. Clasifica cada símbolo pedido en cumplido / **incumplido** (el `.config` dice otra cosa) /
341ausente (el kernel ni lo menciona: la bandera fue un no-op), con la procedencia de quién lo pidió, y
342sale distinto de cero si el config no honra el plan.
343
344## 11. Paso 3 hecho: el gate de no-regresión, por objetivo
345
346La pieza que faltaba no era el gate sino el **mapa driver → símbolo**: el kernel sabe qué driver
347tiene bindeado cada dispositivo, pero no de qué `CONFIG_*` salió. Esa relación sólo existe en los
348Makefiles de kbuild (`obj-$(CONFIG_SND_HDA_INTEL) += snd-hda-intel.o`). Leídas **15 789 reglas en
3493182 Makefiles**.
350
351Dos trampas de nombres que, sin resolver, harían que el gate no encontrara nada y dijera que todo
352está bien — el peor resultado posible para un portón:
353- el módulo cargado usa `_` donde el fichero usa `-` (`snd-hda-intel.o` → `snd_hda_intel`);
354- un módulo puede salir de **varios** símbolos, y sobrevive si sobrevive cualquiera.
355
356**La prueba de que el portón está en el sitio correcto**, con el mismo plan (`sin-usb` +
357`sin-entrada-humana` + `sin-graficos` + `sin-wifi`) y el hardware real de gioser:
358
359| objetivo | veredicto | detalle |
360|---|---|---|
361| `qemu-serial` | ✓ pasa | 5 pérdidas AUTORIZADAS (`hid-generic`, `psmouse`, `usbhid`, `virtio_gpu`, `xhci_hcd`) |
362| `metal-escritorio` | ✗ bloquea | las mismas 5, como REGRESIONES, cada una con el bundle culpable |
363
364Un gate global habría rechazado `recipes/linux.toml`, que está sana.
365
366**Y lo que el gate no puede comprobar, lo dice.** De los 38 drivers bindeados en gioser, 15 no se
367pudieron mapear a ningún símbolo (built-ins de núcleo como `pcieport` o `serial8250`, cuyo nombre de
368driver no coincide con el del módulo). Quedan listados como **sin comprobar**, no como aprobados.
369
370## 12. La prueba contra un `olddefconfig` de verdad (2026-08-10)
371
372Hasta acá todo era análisis: la clausura decía qué debía morir, y nadie lo había contrastado con el
373resolvedor real. **No hace falta construir un kernel para hacerlo**: lo caro es la fase `compile`
374(35–60 min); la cadena entera del armador —fragmento → `scripts/config` → `olddefconfig` →
375diff-back— vive en `configure` y corre en segundos.
376
377Método: árbol 6.16.12 extraído entero, `make defconfig` como base (que **sí** trae wifi, audio y los
378fs encendidos — el config de `linux.toml` no sirve de base porque ya los apaga a mano), el fragmento
379del plan, y `olddefconfig`. Ground truth = los símbolos que pasaron de encendidos a apagados.
380
381| refinamiento | clausura | aciertos | **sobra** | falta |
382|---|---|---|---|---|
383| sólo `depends on` en inversa | 1775 | 64/68 | **0** | 4 |
384| + huérfanos de `select` | 1794 | 65/68 | **0** | 3 |
385| + comparaciones `X = y` / `X != n` | 1795 | 65/68 | **0** | 3 |
386
387**Sobra 0 en las tres.** El predictor nunca dice que muere algo que sobrevive, que es la única
388dirección en la que se puede equivocar sin fabricar un ladrillo.
389
390### Los dos refinamientos que salieron de ahí
391- **Huérfanos de `select`.** Un símbolo **sin prompt** no se puede marcar a mano: sólo entra por
392  `select`. Si todos sus selectores caen, él también, aunque nadie dependa de él. Era el caso de
393  `ACPI_NHLT`. Se exige ≥1 selector: sin ninguno entra por un `default`, y darlo por muerto mataría
394  media tabla.
395- **`X = y` sí es una dependencia dura.** Medio `drivers/video/fbdev` declara su dependencia de `FB`
396  como `depends on (FB = y) && ARM`. Tratar toda comparación como opaca dejaba esos drivers fuera.
397  `X = n` sigue fuera a propósito: con `X` en `n` es **verdadera**. El efecto colateral fue limpiar
398  el ruido: las fugas `select` sin declarar de `sin-graficos` cayeron de 8+ a 1.
399
400### Los 3 que faltan no son un fallo: son otra pregunta
401`CRYPTO_LIB_ARC4`, `REGMAP` y `SYSTEM_DATA_VERIFICATION` se apagaron en ese `.config`, pero tienen
402selectores **fuera** de la clausura (`PPP_MPPE`, 111 usuarios más de `REGMAP`, `MODULE_SIG_FORMAT`…).
403Se quedaron sin usuarios *en esta configuración*; encendé PPP y `CRYPTO_LIB_ARC4` vuelve.
404
405> **«Inalcanzable» y «apagado ahora» no son lo mismo.** La clausura contesta la primera, que es la
406> que define un bundle. Meter los otros tres sería afirmar que el bundle los mata, y no es cierto.
407
408### Y un bug que sólo aparece corriendo el resolvedor
409El diff-back contaba como **promesa incumplida** todo símbolo pedido que no apareciera en el
410`.config`. Pero Kconfig **no emite** un símbolo cuyas dependencias no se cumplen: un `-d WLAN` cuya
411raíz ya cayó simplemente no sale. Con esa cuenta, un plan perfecto se reportaba roto (2 falsos
412incumplidos de 16). Ahora se separa: ausente + se pedía apagar = **éxito**; ausente + se pedía
413encender = **fallo**. El diff-back de la corrida real sale **14 cumplidos, 0 incumplidos**.
414
415### El guardián que faltaba: las cuatro recetas no son el mismo kernel
416`linux` y `linux-metal` van por **6.16.12**; `linux-metal-dual` y `linux-generic` por **7.1.2**.
417Planear una contra el árbol de la otra calcularía clausuras sobre símbolos que ahí no existen, y
418saldría sin ruido. `KconfigTree` ahora lee su propia versión del `Makefile` de arriba y `plan`
419**falla** si no coincide con la de la receta (los diagnósticos sólo avisan).
420
421## 13. Caso de punta a punta: un kernel a medida de gioser (2026-08-11)
422
423La primera vez que el armador se usa para lo que existe. Máquina: Hetzner vServer, AMD EPYC-Rome,
4244 vCPU, 20 dispositivos PCI, 38 drivers bindeados.
425
426**Base: `linux-metal`, no `linux`.** El `probe` decide: `linux` apaga USB, HID e INPUT a propósito
427(es el kernel de QEMU con consola serie) y gioser tiene `xhci_hcd`, `usbhid` e `i8042` bindeados;
428`linux-metal` conserva los tres, más virtio, ext4, vfat y EFI. Elegir la base es la primera decisión
429y el `probe` la contesta con datos, no con intuición.
430
431**Selección:** 13 bundles y 3 perillas → **34 banderas**, clausura de 3054 símbolos.
432
433| | símbolos encendidos |
434|---|---|
435| `linux-metal` tal cual | 1805 |
436| derivada para gioser | **1647** (−158) |
437
438Segunda validación de la clausura contra el `olddefconfig` real, ahora sobre otra base y con 13
439bundles: **154 aciertos, SOBRA 0**, faltan 18 (todos helpers sin prompt del tipo `DRM_DISPLAY_*`,
440`CRYPTO_LIB_ARC4` — la misma clase «apagado ahora ≠ inalcanzable» del §12).
441
442### Lo que sólo se ve midiendo esta máquina
443El gate contra el `.config` producido, con `/proc/config.gz` de referente, encontró **cuatro
444pérdidas que ningún bundle causaba**: `aer`, `iTCO_wdt`, `lpc_ich` y `pcspkr` están encendidos en el
445kernel que corre y **`linux-metal` no los enciende nunca**. No era una regresión del plan: era un
446hueco de la receta base, invisible para el gate del §11 por construcción.
447
448Lo mismo con `VIRTIO_BALLOON` y `HW_RANDOM_VIRTIO`: **ninguna receta de kernel del repo los
449enciende** y en gioser los dos están bindeados. Un kernel de takana arranca igual en esta máquina,
450pero la VM se queda sin globo de memoria y sin la entropía del anfitrión.
451
452⇒ Tres entradas nuevas de catálogo, todas nacidas de medir: bundle `sin-gpu-intel` (i915 es de los
453drivers más grandes del kernel y en una VM con virtio-gpu no sirve) y perillas `invitado-virtio` y
454`plataforma-pc`. Con ellas el gate pasa: **✓ ningún dispositivo en uso se queda sin driver.**
455
456### El artefacto: construido, sellado y verificado
457
458```
459b3:8ffd57109a2f7102c1bbbbc91cf0e729047312eee1a4e3fdb8b178680cc94264-linux-gioser
460```
461
462| | `linux-metal` | derivada gioser |
463|---|---|---|
464| `bzImage` | 14,5 MB | **11,1 MB** (−23%) |
465| símbolos encendidos | 1805 | 1647 |
466
467- **diff-back contra el artefacto: 35 cumplidos, 0 incumplidos.**
468- **gate contra el artefacto** (referente `/proc/config.gz`): ✓ ningún dispositivo en uso se queda
469  sin driver.
470
471### La prueba de 30 s predijo el build de 45 min
472El `.config` del artefacto y el de la prueba en seco (§4.bis del runbook) difieren en **14 líneas, y
473ni una es funcional**: son las siete versiones del toolchain que el kernel graba en su propio
474config.
475
476```
477< CONFIG_CC_VERSION_TEXT="gcc (Alpine 15.2.0) 15.2.0"     ← el lab anclado
478> CONFIG_CC_VERSION_TEXT="gcc (GCC) 16.1.1 20260725"      ← el host
479… GCC_VERSION, AS_VERSION, LD_VERSION, RUSTC_VERSION, RUSTC_LLVM_VERSION, PAHOLE_VERSION
480```
481
482Cero símbolos de diferencia. ⇒ **la prueba barata es un sustituto fiel del build** para todo lo que
483el armador decide.
484
485Y de regalo, la mecánica exacta de por qué el lab tiene que estar en `hash_inputs`: **el kernel
486graba la versión de su compilador DENTRO del `.config`**, y el `.config` va dentro del artefacto. No
487es que el lab «influya» en el resultado — es que **el lab está literalmente en el contenido
488sellado**. (`PAHOLE_VERSION=0` en el artefacto confirma además lo que dice la cabecera de
489`linux.toml`: el toolchain del lab no trae pahole.)
490
491### El paralelismo entra en `hash_inputs`, y hay que saberlo
492gioser estaba al borde del OOM (swap 3611/4095, 12 `oom-kill` previos) **y hospeda gitea**, en una
493máquina marcada como fija y sin backup. Bajar el build a `-j1` es lo prudente — y cambia el hash:
494
495```
496-j"$(nproc)"   b3:10a60ceb…
497-j1            b3:8ffd5710…
498```
499
500El paralelismo **no cambia un byte del `bzImage`**, pero el texto de la fase `compile` está en
501`hash_inputs`. Por eso el `$(nproc)` de las recetas base no es estilo: mantiene el texto
502**independiente de la máquina** y evita bifurcar la identidad del artefacto por el número de cores.
503**Si hay que capar el paralelismo, va FUERA de la receta** (entorno, cgroup, `nice`); editando la
504fase, cada máquina lenta se fabrica su propio hash. Es el reverso exacto del caso `USB4`: allá
505cambió el texto y no el `.config`; acá cambia el texto y no el binario.
506
507### Un hallazgo de regalo
508`recipes/linux.toml` apaga **`REISERFS_FS`, que no existe en 6.16.12** (upstream lo retiró). El
509`scripts/config -d REISERFS_FS` es un no-op que nadie vio. Es exactamente el modo de fallo que el
510**diff-back** (paso 4 del §8) existe para atrapar, encontrado antes de implementarlo: hoy un
511`-e`/`-d` sobre un símbolo inexistente **se pierde en silencio**.

22-configurador-kernel.md

SDD 22 — Configurador de kernel por niveles: respuesta al handoff de tawasuyu

Escrito 2026-08-07, en respuesta a tawasuyu/HANDOFF-KERNEL-CONFIG-A-HAMMER.md.

El handoff avisa de su propio método: «nada de lo que sigue está verificado contra el código de takana — si un dato de takana lo contradice, manda takana». Este documento es esa verificación. No es una aprobación ni un plan de implementación: contesta las seis preguntas de su §9 con evidencia, corrige lo que estaba mal, y señala dos objeciones técnicas que cambian el diseño.

Veredicto corto: la idea se sostiene y el sustrato está más listo de lo que el handoff supone — el mecanismo de §6 («la app nunca escribe un .config») ya es exactamente como funciona hoy. Pero hay un hecho estructural que el handoff no podía conocer y que reordena todo, y una objeción técnica a §2.1 que hay que resolver antes de escribir una línea.


1. El hecho que reordena todo: el config ES la identidad del artefacto

En takana, Recipe::hash_inputs es una lista blanca, y las fases de build entran en ella. El config del kernel vive dentro de la fase configure de recipes/linux*.toml. Por lo tanto:

Cambiar un solo símbolo de config cambia el ArtifactHash del kernel.

Esto no es un detalle de implementación: es la bisagra del diseño, y corta en los dos sentidos.

A favor, y fuerte: §2.2 (config atestado por huella de hardware) sale casi gratis. No hay que inventar un mecanismo para «atestar un config»: cada config ya es un artefacto direccionado por contenido, reproducible bit a bit y firmable con la maquinaria que existe. Un «config que booteó en esta huella» es, literalmente, un ArtifactHash + una firma. El handoff intuyó que takana tenía el sustrato; lo tiene más de lo que creía.

En contra, y también fuerte: la explosión de builds que §2.2 quiere evitar no es hipotética, es aritmética visible en el disco. Cada variante de config es un artefacto nuevo en el store, y el store ya crece 24 G por ciclo de trabajo sólo con las variantes que producimos hoy sin querer. Con N1 (~40–60 bundles) el espacio combinatorio es 2⁴⁰; con «perfiles publicados» —que es lo que el handoff propone en §8— es un puñado. La restricción de §8 no es una preferencia de diseño: es una condición de supervivencia del disco, y conviene decirlo así en la UI.

Consecuencia práctica que hay que aceptar desde el día uno: una «perilla» de la UI no es un parámetro de runtime, es una edición de receta. No hay forma de tener un config variable sin generar recetas. El diseño correcto es que takana kernel plan emita una receta derivada (con su propio hash) en vez de fingir que el kernel es un binario parametrizable.


2. Las seis preguntas del §9, contestadas con evidencia

P1 — ¿Las recetas de kernel aceptan fragmentos o sólo defconfig completo?

Ya son fragmentos, y con la forma que §6 prescribe. recipes/linux.toml, fase configure:

make ARCH=x86_64 defconfig && \
scripts/config -d MODULE_SIG -d DEBUG_INFO_BTF -e SECURITY_LANDLOCK -e AUDIT \
  -d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL  … && \
make ARCH=x86_64 olddefconfig

Base + deltas por símbolo + resolución por el olddefconfig del propio kernel. La app nunca escribe un .config. La única diferencia con el handoff es el vehículo: scripts/config -e/-d en vez de ficheros de fragmento y merge_config.sh. Son equivalentes y scripts/config es más fácil de generar programáticamente. Media implementación de §6 ya está en producción.

Falta la otra mitad, que es la que da la UI veraz: el diff-back. Hoy nada comprueba que los símbolos pedidos hayan sobrevivido a olddefconfig. Un -e FOO cuya dependencia no se cumple se pierde en silencio — el mismo patrón de fallo que el license que caía dentro de [deps].

P2 — ¿El build es bit-reproducible con config variable?

Sí, y por construcción. El config no es un parámetro externo: es parte de la entrada hasheada (§1). Cada config es su propia receta con su propio hash y su propia garantía de reproducción. No hay un «kernel» cuya reproducibilidad dependa de congelar el config; hay N kernels, cada uno congelado. ⇒ §2.2 puede prefirmar artefactos, no sólo configs.

P3 — ¿Cuánto tarda un kernel completo? 🚨

Declarado en las propias recetas: linux ~35 min, linux-metal ~45 min, linux-generic ~60 min en la máquina de referencia. En la más lenta del parque, más.

Esto mata la bisección ingenua de §5. ~6 rebuilds × 45 min = 4,5 horas de la máquina del usuario para encontrar un bundle culpable. Nadie espera eso, y menos con la máquina que acaba de no arrancar. El handoff ya preveía el riesgo («si es caro, tope duro de iteraciones»); la respuesta es que es caro. Ver §4, donde propongo una bisección que no recompila.

P4 — ¿harkaq puede prestar su motor de clausura?

El patrón sí; el código no. crates/hammer-build/src/harkaq.rs deriva su política enumerando fichero a fichero los artefactos de las deps declaradas — es una clausura sobre el grafo de paquetes materializado en el store, con semántica de rutas de filesystem. Kconfig es otro grafo, con otra semántica y ~15 000 símbolos. Reusar la idea («política = clausura, no lista») es correcto y está validado; reusar la implementación no. Hace falta un lector de Kconfig — y ahí está la objeción del §3.

P5 — ¿El índice firmado admite un eje más (la huella)?

Sí, y sin romper nada. PackageEntry (crates/hammer-core/src/repo.rs) es un struct serde cuyos campos opcionales llevan #[serde(default, skip_serializing_if = ...)]. Añadir fingerprint: Option<String> es compatible hacia atrás: los índices viejos siguen parseando y los clientes viejos ignoran el campo. Es exactamente el truco que hizo pagable el campo license hoy mismo.

P6 — El prerrequisito de PLAN-KIKIN §4.bis

Estaba dado por pendiente y está hecho en dos de tres. Medido:

recetaSECURITY_LANDLOCKAUDITIO_URINGBPF_SYSCALL
linux-metal✅ -e✅ -e✅ -e✅ -e
linux-generic✅ -e✅ -e✅ -e✅ -e
linux✅ -e✅ -e❌ sólo en un comentario❌ sólo en un comentario

⚠️ Y la forma de medirlo importa: grep IO_URING recipes/linux.toml da positivo porque la palabra aparece en un comentario. Hay que mirar los -e/-d dentro del bloque configure. Es el mismo error que hoy infló el conteo de licencias (grep -l license contaba comentarios y nombres de paquete) y el que hace mentir a la regla del .a no-PIC. En este repo, grep sobre ficheros con comentarios densos no es una medición.

⇒ Queda pendiente sólo linux, que es el kernel de QEMU/serial.


3. Objeción técnica a §2.1: «clausura» sobre Kconfig no está bien definida todavía

La propuesta —«todo lo alcanzable desde CONFIG_WIRELESS menos lo alcanzable desde lo que sí quiero»— asume un grafo con aristas de un solo tipo. Kconfig tiene al menos tres, con semánticas incompatibles:

«Alcanzable» por depends on y «alcanzable» por select dan conjuntos distintos y ninguno de los dos es la respuesta. Un driver de otro subsistema puede select un helper de wireless sin ser wireless: cae dentro de la clausura ingenua y apagarlo rompe algo que el usuario sí quería. Y al revés, select puede meter símbolos wireless desde fuera de la clausura.

Esto no invalida §2.1 — lo convierte en la parte con contenido técnico real, tal como el propio handoff dice en su §10. Pero hay que definir la semántica de arista antes de escribir el predicado, y la definición hay que validarla contra algo.

Y tenemos contra qué validarla, hoy y gratis

El config actual de linux.toml ya es un juego de bundles N1 escrito a mano. Literalmente:

-d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL      ← el bundle «no necesito wifi»
-d SOUND -d SND                                            ← «no necesito audio»
-d DRM -d DRM_I915 -d DRM_AMDGPU -d DRM_NOUVEAU -d FB      ← «sin gráficos»
-d XFS_FS -d BTRFS_FS -d F2FS_FS -d JFS_FS -d REISERFS_FS  ← «sólo ext4»

Es un corpus de verdad-de-campo: alguien decidió a mano qué símbolos componen «no necesito wifi», y ese kernel arranca. La prueba de §2.1 es directa y barata: ¿la clausura calculada de CONFIG_WIRELESS reproduce ese conjunto? Si sobra o falta mucho, la semántica de arista está mal y nos enteramos en una tarde, sin compilar nada. Refuerza el §10 del handoff: eso se hace antes que nada, junto al modo reversa.


4. Lo que agrego: bisección sin recompilar

Dado P3 (35–60 min por kernel), la bisección de §5 cuesta horas. Pero la mayoría de los bundles N1 son drivers, y un driver puede ser =m en vez de =y.

Propuesta: un kernel «superset» con todo lo bisecable como módulo. Los bundles N1 de tipo hardware no se resuelven entonces recompilando, sino con una lista de bloqueo de módulos en la línea de arranque. La bisección deja de ser 6 rebuilds (4,5 h) y pasa a ser 6 reinicios (minutos), sobre un kernel que ya está en el árbol de generaciones.

Consecuencias:

5. Lo que agrego: el gate de no-regresión (§6) debe ser por objetivo, no global

El handoff lo llama la mejor relación valor/esfuerzo y estoy de acuerdo. Pero el config actual de linux.toml apaga USB_SUPPORT, HID, HID_SUPPORT, INPUT_MOUSE, INPUT_TOUCHSCREEN. Ese kernel no puede recibir teclado en metal — y es correcto, porque es el kernel de QEMU con consola serie. Un gate global «todo dispositivo en uso debe seguir teniendo driver» rechazaría una receta sana.

⇒ El gate necesita el objetivo declarado (qemu-serial / metal / servidor) y una noción de «dispositivos en uso de la máquina destino», que no siempre es la máquina donde se construye. Sigue siendo la propuesta más barata de las nueve; sólo que el portón se pone en el sitio correcto.

6. Dos huecos del handoff que conviene cerrar antes

«Booteó» no es una aserción suficiente para atestar (§2.2). Un kernel puede arrancar y dejarte sin red, sin wifi o sin teclado — que es justo el desastre que §6 quiere evitar. Si la atestación dice sólo «booteó en esta huella», estamos firmando la clase de fallo que más duele. La atestación debería llevar qué se comprobó: qué dispositivos de la huella quedaron con driver bindeado después de arrancar. Es el mismo dato que ya calcula el gate de §6 ⇒ una implementación, dos usos.

El catálogo de bundles necesita las tres etiquetas de nivel, no dos. El handoff avisa en §5 que «la mitad de N2 no es Kconfig sino variables de receta». Con §1 en la mano eso es más grave de lo que parece: las perillas de receta (LTO, -march, zig_version) cambian el hash igual que los símbolos, pero se aplican en otra fase y fallan de otra forma. La UI tiene que saber de qué lado cae cada perilla o prometerá diffs que no puede explicar.


7. Qué contesto al reparto (§7)

De acuerdo con el reparto tal como está. Dos precisiones desde este lado:

8. Orden que propongo (modifica el §10 del handoff)

  1. Modo reversa (#9) — tal cual lo propone: cero riesgo, valida el catálogo contra un kernel real.
  2. Validar la clausura contra el config actual (§3 de este documento) — sale gratis, usa un corpus que ya existe, y decide si §2.1 es viable. Va antes que el gate, porque si la clausura no reproduce los bundles hechos a mano, el diseño de N1 cambia.
  3. Gate de no-regresión (#6), por objetivo (§5 de este documento).
  4. Diff-back (la mitad que falta de §6 del handoff): barato, y sin él la UI miente.
  5. Recién después, #1 (clausuras) con la semántica ya decidida.

No implementar todavía. Este documento existe para que la decisión se tome con los seis datos verificados, no con las suposiciones del handoff — que su autor, correctamente, marcó como tales.


ADENDA — 2026-08-10: implementación en marcha

El usuario decidió. Lo que sigue ya no es análisis: es lo medido contra el código escrito. Se sigue el orden del §8.

9. Paso 2 hecho: la clausura SÍ reproduce los bundles a mano

takana kernel (crates/hammer-core/src/kernel/, crates/hammer-cli/src/kernel_cmd.rs) trae un lector de Kconfig de sólo análisis — la regla dura del §6 del handoff se respeta literalmente: no hay solver, el .config lo sigue produciendo olddefconfig.

Salud del lector, sobre linux-6.16.12 (takana kernel stats):

ficherossímbolosvisiblescon ayudaaristas selectaristas implyavisos de parseo
164618 21215 16914 86615 1654450

La semántica de arista, resuelta

El §3 pedía definirla antes de escribir el predicado. Quedó así, y las dos mitades importan:

La medición que decide §2.1

Corpus: el juego de bundles N1 escrito a mano en la fase configure de recipes/linux.toml, sobre un kernel que arranca. Pregunta: ¿la clausura calculada desde la raíz mínima reproduce lo que escribió el humano?

takana kernel closure --kconfig <árbol> WIRELESS --fixpoint
símbolos
clausura estricta de WIRELESS (sólo depends on)350
punto fijo (cierra las 3 fugas select: WLAN, IWLEGACY, GELIC_WIRELESS)406
bundle -d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL hecho a mano421
sobra (apagaría algo que el humano dejó vivo)0
falta15

Sobra cero. La clausura no toca un solo símbolo que el humano quisiera conservar — que era el riesgo real, porque sobrar es hacer un ladrillo. Cierra en una ronda.

Los 15 que faltan son todos de RFKILL (RFKILL{,_GPIO,_INPUT,_LEDS} y los drivers de laptop que dependen de él: DELL_RBTN, IDEAPAD_LAPTOP, MSI_LAPTOP, AMILO_RFKILL…). Y RFKILL no es wifi: es el interruptor de radio compartido, que bluetooth y NFC declaran con el mismo idioma (depends on RFKILL || !RFKILL en net/{bluetooth,nfc,wireless}/Kconfig). Que quede fuera no es un fallo de la clausura — es que el humano apagó dos bundles a la vez («no necesito wifi» y «no necesito radios») y los escribió en la misma línea. ⇒ el catálogo N1 necesita «sin radios» como entrada propia. Nótese que ese idioma es un ||: la regla conservadora lo excluye de los duros, que es exactamente lo correcto.

⇒ §2.1 es viable y la objeción del §3 queda resuelta, con una corrección al diseño: un bundle no es clausura(raíz) sino clausura(raíz) + el punto fijo sobre select, y las fugas se aprueban a mano una vez, no en cada release.

Y el punto fijo también dice cuándo NO

No siempre cierra barato, y eso es información, no un fallo. Medido sobre los otros tres bundles a mano de linux.toml:

bundle a manoclausurafugas selectqué las causa
WLAN WIRELESS CFG80211 MAC80211 RFKILL4210cerrado
SOUND SND132319 sobre 7 destinosDRM_I915/DRM_NOUVEAU/DRM_AMD_DC hacen select del códec HDMI
DRM DRM_I915 … FB AGP77015 sobre 6 destinosdrivers USB-C (TYPEC_*) hacen select DRM_AUX_BRIDGE
XFS_FS BTRFS_FS … NTFS3_FS461NTFS3_FS ← NTFS_FS

Para cerrar «no necesito audio» hay que tragarse drivers gráficos. En linux.toml sale gratis porque los gráficos también están apagados, pero en un perfil de escritorio no: ahí la fuga es una decisión, y por eso el punto fijo la muestra en vez de aplicarla.

10. Pasos 1 y 4 hechos: modo reversa, plan y diff-back

Modo reversa (takana kernel probe) sobre gioser, con el catálogo docs/state/kernel-bundles.toml (15 bundles N1, 8 perillas N2): 10 551 símbolos declarados, 20 dispositivos PCI, 38 drivers bindeados, 7 bundles con capacidad que este hardware no usa. Y avisa de lo que el propio caso destapó: el config vivo es de la serie 7.1 y el catálogo se revisó contra la 6.16 ⇒ las clausuras son aproximadas. Se dice en vez de callarlo.

Plan (takana kernel plan): emite una receta derivada, como manda §1. Medido sobre recipes/linux.toml con sin-wifi + sin-audio + solo-ext4 + jaula-y-eio-moderna:

banderas emitidas16
símbolos que caen con su clausura1775
ArtifactHash de la baseb3:cb926743…
ArtifactHash de la derivadab3:47a52b2e…

Dos hashes distintos para el mismo código fuente: el config es la identidad, y ahora se ve. El plan lleva su hash dentro ⇒ la UI puede decir «esto ya está construido y firmado» sin construir.

Tres detalles del plan que no eran obvios:

Diff-back (takana kernel diff-back --plan … --config …): la mitad que faltaba del §6 del handoff. Clasifica cada símbolo pedido en cumplido / incumplido (el .config dice otra cosa) / ausente (el kernel ni lo menciona: la bandera fue un no-op), con la procedencia de quién lo pidió, y sale distinto de cero si el config no honra el plan.

11. Paso 3 hecho: el gate de no-regresión, por objetivo

La pieza que faltaba no era el gate sino el mapa driver → símbolo: el kernel sabe qué driver tiene bindeado cada dispositivo, pero no de qué CONFIG_* salió. Esa relación sólo existe en los Makefiles de kbuild (obj-$(CONFIG_SND_HDA_INTEL) += snd-hda-intel.o). Leídas 15 789 reglas en 3182 Makefiles.

Dos trampas de nombres que, sin resolver, harían que el gate no encontrara nada y dijera que todo está bien — el peor resultado posible para un portón:

La prueba de que el portón está en el sitio correcto, con el mismo plan (sin-usb + sin-entrada-humana + sin-graficos + sin-wifi) y el hardware real de gioser:

objetivoveredictodetalle
qemu-serial✓ pasa5 pérdidas AUTORIZADAS (hid-generic, psmouse, usbhid, virtio_gpu, xhci_hcd)
metal-escritorio✗ bloquealas mismas 5, como REGRESIONES, cada una con el bundle culpable

Un gate global habría rechazado recipes/linux.toml, que está sana.

Y lo que el gate no puede comprobar, lo dice. De los 38 drivers bindeados en gioser, 15 no se pudieron mapear a ningún símbolo (built-ins de núcleo como pcieport o serial8250, cuyo nombre de driver no coincide con el del módulo). Quedan listados como sin comprobar, no como aprobados.

12. La prueba contra un olddefconfig de verdad (2026-08-10)

Hasta acá todo era análisis: la clausura decía qué debía morir, y nadie lo había contrastado con el resolvedor real. No hace falta construir un kernel para hacerlo: lo caro es la fase compile (35–60 min); la cadena entera del armador —fragmento → scripts/config → olddefconfig → diff-back— vive en configure y corre en segundos.

Método: árbol 6.16.12 extraído entero, make defconfig como base (que sí trae wifi, audio y los fs encendidos — el config de linux.toml no sirve de base porque ya los apaga a mano), el fragmento del plan, y olddefconfig. Ground truth = los símbolos que pasaron de encendidos a apagados.

refinamientoclausuraaciertossobrafalta
sólo depends on en inversa177564/6804
+ huérfanos de select179465/6803
+ comparaciones X = y / X != n179565/6803

Sobra 0 en las tres. El predictor nunca dice que muere algo que sobrevive, que es la única dirección en la que se puede equivocar sin fabricar un ladrillo.

Los dos refinamientos que salieron de ahí

Los 3 que faltan no son un fallo: son otra pregunta

CRYPTO_LIB_ARC4, REGMAP y SYSTEM_DATA_VERIFICATION se apagaron en ese .config, pero tienen selectores fuera de la clausura (PPP_MPPE, 111 usuarios más de REGMAP, MODULE_SIG_FORMAT…). Se quedaron sin usuarios en esta configuración; encendé PPP y CRYPTO_LIB_ARC4 vuelve.

«Inalcanzable» y «apagado ahora» no son lo mismo. La clausura contesta la primera, que es la que define un bundle. Meter los otros tres sería afirmar que el bundle los mata, y no es cierto.

Y un bug que sólo aparece corriendo el resolvedor

El diff-back contaba como promesa incumplida todo símbolo pedido que no apareciera en el .config. Pero Kconfig no emite un símbolo cuyas dependencias no se cumplen: un -d WLAN cuya raíz ya cayó simplemente no sale. Con esa cuenta, un plan perfecto se reportaba roto (2 falsos incumplidos de 16). Ahora se separa: ausente + se pedía apagar = éxito; ausente + se pedía encender = fallo. El diff-back de la corrida real sale 14 cumplidos, 0 incumplidos.

El guardián que faltaba: las cuatro recetas no son el mismo kernel

linux y linux-metal van por 6.16.12; linux-metal-dual y linux-generic por 7.1.2. Planear una contra el árbol de la otra calcularía clausuras sobre símbolos que ahí no existen, y saldría sin ruido. KconfigTree ahora lee su propia versión del Makefile de arriba y plan falla si no coincide con la de la receta (los diagnósticos sólo avisan).

13. Caso de punta a punta: un kernel a medida de gioser (2026-08-11)

La primera vez que el armador se usa para lo que existe. Máquina: Hetzner vServer, AMD EPYC-Rome, 4 vCPU, 20 dispositivos PCI, 38 drivers bindeados.

Base: linux-metal, no linux. El probe decide: linux apaga USB, HID e INPUT a propósito (es el kernel de QEMU con consola serie) y gioser tiene xhci_hcd, usbhid e i8042 bindeados; linux-metal conserva los tres, más virtio, ext4, vfat y EFI. Elegir la base es la primera decisión y el probe la contesta con datos, no con intuición.

Selección: 13 bundles y 3 perillas → 34 banderas, clausura de 3054 símbolos.

símbolos encendidos
linux-metal tal cual1805
derivada para gioser1647 (−158)

Segunda validación de la clausura contra el olddefconfig real, ahora sobre otra base y con 13 bundles: 154 aciertos, SOBRA 0, faltan 18 (todos helpers sin prompt del tipo DRM_DISPLAY_*, CRYPTO_LIB_ARC4 — la misma clase «apagado ahora ≠ inalcanzable» del §12).

Lo que sólo se ve midiendo esta máquina

El gate contra el .config producido, con /proc/config.gz de referente, encontró cuatro pérdidas que ningún bundle causaba: aer, iTCO_wdt, lpc_ich y pcspkr están encendidos en el kernel que corre y linux-metal no los enciende nunca. No era una regresión del plan: era un hueco de la receta base, invisible para el gate del §11 por construcción.

Lo mismo con VIRTIO_BALLOON y HW_RANDOM_VIRTIO: ninguna receta de kernel del repo los enciende y en gioser los dos están bindeados. Un kernel de takana arranca igual en esta máquina, pero la VM se queda sin globo de memoria y sin la entropía del anfitrión.

⇒ Tres entradas nuevas de catálogo, todas nacidas de medir: bundle sin-gpu-intel (i915 es de los drivers más grandes del kernel y en una VM con virtio-gpu no sirve) y perillas invitado-virtio y plataforma-pc. Con ellas el gate pasa: ✓ ningún dispositivo en uso se queda sin driver.

El artefacto: construido, sellado y verificado

b3:8ffd57109a2f7102c1bbbbc91cf0e729047312eee1a4e3fdb8b178680cc94264-linux-gioser
linux-metalderivada gioser
bzImage14,5 MB11,1 MB (−23%)
símbolos encendidos18051647

La prueba de 30 s predijo el build de 45 min

El .config del artefacto y el de la prueba en seco (§4.bis del runbook) difieren en 14 líneas, y ni una es funcional: son las siete versiones del toolchain que el kernel graba en su propio config.

< CONFIG_CC_VERSION_TEXT="gcc (Alpine 15.2.0) 15.2.0"     ← el lab anclado
> CONFIG_CC_VERSION_TEXT="gcc (GCC) 16.1.1 20260725"      ← el host
… GCC_VERSION, AS_VERSION, LD_VERSION, RUSTC_VERSION, RUSTC_LLVM_VERSION, PAHOLE_VERSION

Cero símbolos de diferencia. ⇒ la prueba barata es un sustituto fiel del build para todo lo que el armador decide.

Y de regalo, la mecánica exacta de por qué el lab tiene que estar en hash_inputs: el kernel graba la versión de su compilador DENTRO del .config, y el .config va dentro del artefacto. No es que el lab «influya» en el resultado — es que el lab está literalmente en el contenido sellado. (PAHOLE_VERSION=0 en el artefacto confirma además lo que dice la cabecera de linux.toml: el toolchain del lab no trae pahole.)

El paralelismo entra en hash_inputs, y hay que saberlo

gioser estaba al borde del OOM (swap 3611/4095, 12 oom-kill previos) y hospeda gitea, en una máquina marcada como fija y sin backup. Bajar el build a -j1 es lo prudente — y cambia el hash:

-j"$(nproc)"   b3:10a60ceb…
-j1            b3:8ffd5710…

El paralelismo no cambia un byte del bzImage, pero el texto de la fase compile está en hash_inputs. Por eso el $(nproc) de las recetas base no es estilo: mantiene el texto independiente de la máquina y evita bifurcar la identidad del artefacto por el número de cores. Si hay que capar el paralelismo, va FUERA de la receta (entorno, cgroup, nice); editando la fase, cada máquina lenta se fabrica su propio hash. Es el reverso exacto del caso USB4: allá cambió el texto y no el .config; acá cambia el texto y no el binario.

Un hallazgo de regalo

recipes/linux.toml apaga REISERFS_FS, que no existe en 6.16.12 (upstream lo retiró). El scripts/config -d REISERFS_FS es un no-op que nadie vio. Es exactamente el modo de fallo que el diff-back (paso 4 del §8) existe para atrapar, encontrado antes de implementarlo: hoy un -e/-d sobre un símbolo inexistente se pierde en silencio.