gioser

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 1184Shell 70Rust 62Markdown 501503 Definitions

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>

7bb612c7b559079896efa00849f02b688a8be1d7 · by Sergio · (3 d ago) · parent 93f2d023d · browse the tree at this commit

Semantic delta · 3

scripts/censo-distro.py

2 files · +73 −3

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'