> 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…
feat(distro): el censo ahora se audita contra el store — y el barrido encontró un bug mío primero
Automatiza el hallazgo que destapó `libarchive`: un renglón puede faltar como RECETA y existir como
BINARIO dentro de otro paquete. El censo indexa los `bin`/`sbin`/`usr/bin`/`usr/sbin` de los
artefactos sellados y marca el renglón como «capacidad cubierta» en vez de dejarlo como hueco
pelado. Antes eso dependía de que yo me acordara de mirar; ahora sale solo en cada corrida.
Hallazgo real: **`ninja` lo instala `samurai`** con ese nombre exacto (1.9.0, probado). No era un
hueco.
⚠ Y LO PRIMERO QUE ENCONTRÓ EL BARRIDO FUE UN DEFECTO DE SÍ MISMO. Mi versión de usar y tirar
barría `/store/*` entero y anunciaba que `unzip` y `hdparm` «ya los instala busybox». Falso: era el
busybox **SUPERADO** (`2a2b1280…`). El vigente (`4bd934f6…`) ya no los trae — la campaña de botar
busybox los recortó. **Un artefacto superado no dice nada de lo que hay en una imagen**, y contarlo
convierte el instrumento que caza huecos falsos en una fábrica de huecos falsos. Es la misma lección
que los «sellados rancios» del static-audit, y la pagué en el mismo barrido que la encontró.
⇒ La versión que entra lee el estado del grafo y toma **sólo lo vigente y sellado**, y excluye los
rootfs ensamblados por nombre (traen el busybox del piso adentro, así que todo applet se proveería a
sí mismo — mismo criterio que el censo de applets).
Y una fila se marca como dudosa, que es más honesto que dejarla contando: **«construcción C
clásica»**. cmake, meson, make y samurai existen y no viajan en ninguna imagen — probablemente a
propósito, porque una imagen de producto no se construye a sí misma, y la receta de `nodejs` ya tomó
esa decisión por escrito para su caso. Lo que «falta» ahí puede no ser un hueco sino un PERFIL DE
DESARROLLO que no existe. Medido y sin declarar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/docs/state/distro-huecos.json b/docs/state/distro-huecos.json
index 81346a66..1bdd1778 100644
--- a/docs/state/distro-huecos.json
+++ b/docs/state/distro-huecos.json
@@ -873,12 +873,14 @@
"pista": "nombre parecido en el catálogo (verificar, NO es equivalencia)"
},
{
+ "cubierto_por": "samurai",
+ "cubierto_porque": "MEDIDO: un artefacto vigente instala un binario con ese nombre exacto. Falta la receta propia, no la capacidad",
"estado": "falta",
"nombre": "ninja",
"parientes": []
}
],
- "porque": "cmake, meson y make existen (sin viajar). ⚠ `gcc` está RETENIDO a propósito (sólo `gcc-libs`) y `clang` existe como `clang18`: eso NO es hueco, es decisión"
+ "porque": "⚠ ESTA FILA ES LA MÁS DUDOSA DEL CHECKLIST y conviene que lo diga: cmake, meson, make y samurai existen y NO viajan en ninguna imagen — probablemente a propósito, porque una imagen de producto no se construye a sí misma. La receta de `nodejs` ya tomó esa decisión por escrito para su caso («acá Node es un compilador más, no software que la distro publique»). ⇒ lo que falta acá puede no ser un hueco sino un PERFIL de desarrollo que no existe. Se deja medido y sin declarar. `ninja` además lo instala `samurai` con ese nombre exacto (1.9.0, medido). Y `gcc` está RETENIDO a propósito (sólo `gcc-libs`), `clang` es `clang18`: eso no es hueco, es decisión"
},
{
"categoria": "vpn y red gestionada",
diff --git a/scripts/censo-distro.py b/scripts/censo-distro.py
index 5c460879..92b2f3ca 100755
--- a/scripts/censo-distro.py
+++ b/scripts/censo-distro.py
@@ -36,6 +36,7 @@ Uso: censo-distro.py [--json docs/state/distro-huecos.json]
"""
import argparse
import glob
+import importlib.util
import json
import os
import sys
@@ -116,8 +117,14 @@ CHECKLIST = {
'empaquetadores de python': ('pip no viaja y no hay forma de instalar una herramienta python',
['pipx', 'uv', 'poetry']),
'construcción C clásica': (
- 'cmake, meson y make existen (sin viajar). ⚠ `gcc` está RETENIDO a propósito (sólo '
- '`gcc-libs`) y `clang` existe como `clang18`: eso NO es hueco, es decisión',
+ '⚠ ESTA FILA ES LA MÁS DUDOSA DEL CHECKLIST y conviene que lo diga: cmake, meson, make y '
+ 'samurai existen y NO viajan en ninguna imagen — probablemente a propósito, porque una '
+ 'imagen de producto no se construye a sí misma. La receta de `nodejs` ya tomó esa decisión '
+ 'por escrito para su caso («acá Node es un compilador más, no software que la distro '
+ 'publique»). ⇒ lo que falta acá puede no ser un hueco sino un PERFIL de desarrollo que no '
+ 'existe. Se deja medido y sin declarar. `ninja` además lo instala `samurai` con ese nombre '
+ 'exacto (1.9.0, medido). Y `gcc` está RETENIDO a propósito (sólo `gcc-libs`), `clang` es '
+ '`clang18`: eso no es hueco, es decisión',
['autoconf', 'automake', 'ninja']),
# ── RED, SEGURIDAD, DATOS ───────────────────────────────────────────────────────────────────
@@ -185,6 +192,56 @@ MUROS = {
}
+# ── ¿LO INSTALA YA ALGÚN ARTEFACTO VIGENTE, CON ESE MISMO NOMBRE? ──────────────────────────────
+# Automatiza el hallazgo que destapó `libarchive`: un renglón puede faltar como RECETA y existir
+# como BINARIO dentro de otro paquete. Se mide contra los `bin`/`sbin`/`usr/bin`/`usr/sbin` de los
+# artefactos sellados, igual que `busybox-censo-proveedores.py`.
+#
+# ⚠ SÓLO ARTEFACTOS VIGENTES, y es un defecto medido de mi primera versión (2026-10-19): barriendo
+# `/store/*` entero, `unzip` y `hdparm` salían «ya los instala busybox» — y era el busybox
+# SUPERADO (`2a2b1280…`). El vigente (`4bd934f6…`) ya no los trae, porque la campaña de botar
+# busybox los recortó. **Un artefacto superado no dice nada de lo que hay en una imagen**, y
+# contarlo convierte el instrumento que caza huecos falsos en una fábrica de huecos falsos.
+# Es la misma lección que los «sellados rancios» del static-audit.
+#
+# ⚠ Y los rootfs ENSAMBLADOS se excluyen por nombre: traen el busybox del piso adentro, así que
+# todo applet se «proveería» a sí mismo. Mismo criterio que el censo de applets.
+ROOTFS = ('product-rootfs', 'product-attested-rootfs', 'stage1-rootfs')
+DIRS_BIN = ('bin', 'sbin', 'usr/bin', 'usr/sbin')
+
+
+def _cargar(nombre, ruta):
+ """Se importa por RUTA porque los ficheros llevan guion y no son módulos importables."""
+ spec = importlib.util.spec_from_file_location(nombre, ruta)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+
+
+def indice_de_binarios(cp):
+ """{nombre de binario: {recetas VIGENTES que lo instalan}}."""
+ hashes, _ = cp.mapa_hashes()
+ vigentes = {}
+ for (cola, nombre), (h, estado) in hashes.items():
+ if not h or estado != 'sealed' or nombre in ROOTFS:
+ continue
+ vigentes.setdefault(nombre, (h or '').replace('b3:', ''))
+ idx = {}
+ for nombre, h in vigentes.items():
+ base = os.path.join('/store', '%s-%s' % (h, nombre))
+ for sub in DIRS_BIN:
+ d = os.path.join(base, sub)
+ if not os.path.isdir(d):
+ continue
+ try:
+ entradas = os.listdir(d)
+ except OSError:
+ continue
+ for e in entradas:
+ idx.setdefault(e, set()).add(nombre)
+ return idx
+
+
def nombres_conocidos():
"""Toda receta alcanzable: catálogo, colas `incoming-*` y `tandas/`."""
n = set()
@@ -220,8 +277,14 @@ def main():
a = ap.parse_args()
import yupana
+ cp = _cargar('cierre_perfiles', os.path.join(RAIZ, 'scripts', 'cierre-perfiles.py'))
conocidos = nombres_conocidos()
en_imagen = {n for (c, n) in yupana.membresia_imagen() if c != 'wanted'}
+ try:
+ binarios = indice_de_binarios(cp)
+ except Exception as e: # sin store legible el censo sigue sirviendo; se dice y se sigue
+ print(' (no pude indexar los binarios del store: %s)' % e)
+ binarios = {}
filas, resumen = [], {'tiene-y-viaja': 0, 'tiene-sin-viajar': 0, 'falta': 0}
for cat, (porque, cands) in CHECKLIST.items():
@@ -239,6 +302,11 @@ def main():
i['muro'], i['muro_porque'] = MUROS[i['nombre']]
if i['nombre'] in CUBIERTOS:
i['cubierto_por'], i['cubierto_porque'] = CUBIERTOS[i['nombre']]
+ elif i['estado'] == 'falta' and i['nombre'] in binarios:
+ quien = sorted(binarios[i['nombre']])
+ i['cubierto_por'] = ', '.join(quien)
+ i['cubierto_porque'] = ('MEDIDO: un artefacto vigente instala un binario con ese '
+ 'nombre exacto. Falta la receta propia, no la capacidad')
print('== qué le falta a la distro para usarse (checklist de JUICIO, estado MEDIDO) ==')
print(' recetas alcanzables: %d · de ellas en alguna imagen: %d'