#!/usr/bin/env python3 # vigia-subcomandos.py — ¿hay herramientas SELLADAS que no sirven porque falta su DRIVER? # # ══ EL PUNTO CIEGO QUE ESTE VIGÍA CUBRE ════════════════════════════════════════════════════════ # `build-state.py` contesta «¿está sellado?», `vigia-sonames.py` contesta «¿algún `.so` que alguien # pide no lo provee nadie?» y `vigia-imagen.py` contesta «¿la imagen se puede USAR?». Ninguno # contesta ésta: **¿este binario sellado sirve para algo por sí solo?** # # Hay una familia entera de herramientas que NO son programas: son SUBCOMANDOS. `protoc-gen-go` es un # plugin que `protoc` ejecuta; `cargo-audit` existe para escribirse `cargo audit`; `kubectl-tree` lo # despacha `kubectl`. Sellados, no vacíos, sin NEEDED colgante, reproducibles — y **inertes**, porque # el driver que los invoca no está en el catálogo. # # ⚠ **Ningún indicador existente puede verlo.** El artefacto tiene contenido (regla 3 ✓), resuelve # sus sonames (vigia-sonames ✓), el grafo lo cuenta como `sealed` (✓) y `verificar-repro` dice que # REPRODUCE (✓). Cuatro verdes sobre algo que no se puede usar. La arista «me ejecuta aquél» no es # una dep de build, así que no existe en el grafo: se descubre sólo cuando alguien va a usarlo. # # ══ CÓMO LO ENCONTRÉ (2026-09-11), que es la razón de que este fichero exista ═══════════════════ # `recipes/protoc-gen-go.toml` llevaba meses sellado y el catálogo NO tenía `protoc`. Un plugin de # protoc sin protoc no hace absolutamente nada. No lo delató ninguna métrica: lo delató ir a escribir # la receta de un servicio gRPC y preguntarse con qué se generan los stubs. # # ══ QUÉ MIDE, Y POR QUÉ ASÍ ════════════════════════════════════════════════════════════════════ # Para cada prefijo de la tabla DRIVERS, junta las recetas que empiezan con él y comprueba que el # **BINARIO** del driver exista dentro de algún artefacto sellado. # # · Se comprueba el BINARIO y no el nombre de la receta a propósito: el driver de `protoc-gen-*` es # `protoc`, que lo publica la receta `protobuf`. Preguntar «¿existe recipes/protoc.toml?» habría # contestado «no» incluso después de cerrarlo, y un vigía que grita cuando ya está arreglado se # empieza a ignorar — que es cómo se muere un guardián ([[guardianes-el-ruido-los-mata]]). # · La tabla es EXPLÍCITA y no una heurística sobre guiones. `git-cliff` es subcomando de git, pero # `gettext-tiny` no es subcomando de `gettext`. Adivinar por el guión da falsos positivos, y un # vigía con falsos positivos no se lee. # # ══ AUTOPRUEBA ═════════════════════════════════════════════════════════════════════════════════ # `--autoprueba` corre los dos controles que hacen falta para creerle a un guardián: uno NEGATIVO # (esconder un driver que sí está ⇒ TIENE que saltar) y uno POSITIVO (sin tocar nada, los drivers # presentes NO deben aparecer). Un guardián que nunca falló no se sabe si sirve. # # Uso: scripts/vigia-subcomandos.py [--autoprueba] # Sale: 0 si no hay inertes · 1 si los hay · 2 si la autoprueba falla import glob import os import textwrap import re import sys RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # prefijo de receta → (binario del driver, quién se supone que lo publica, para el mensaje) DRIVERS = { "cargo-": ("cargo", "el toolchain de Rust"), "kubectl-": ("kubectl", "una receta kubectl"), "git-": ("git", "recipes/git.toml"), "helm-": ("helm", "recipes/helm.toml"), "protoc-gen-": ("protoc", "recipes/protobuf.toml"), "docker-": ("docker", "una receta docker"), } # Huecos ya CONOCIDOS y con su motivo, para que el fichero diario no diga «44 inertes» a secas todos # los días. Un número grande y constante, sin explicación al lado, entrena a saltearse el informe — # y entonces el hueco NUEVO, que es el que importa, pasa desapercibido entre el ruido del viejo. # Se imprimen como `•` y NO cuentan para el código de salida. CONOCIDOS = { "cargo": "PENDIENTE DE DECISIÓN, no olvido. `cargo`/`rustc` viven hoy en el LAB y en la cadena " "de selfhost (mrustc→1.91.1), no como receta del catálogo. Empaquetarlos es una " "decisión de TOOLCHAIN —qué Rust publica la distro, y desde qué cadena— no trabajo de " "granja. Hasta que se tome, estos 44 son catálogo sin consumidor.", } def artefactos(): """Todos los directorios de artefacto del store, por nombre de receta. ⚠ El nombre se saca con una EXPRESIÓN ANCLADA, no con `glob('store/*-')`: ese glob hace coincidencia por sufijo, así que `*-go` casa con `…-protoc-gen-go` y contesta que `go` está sellado cuando no lo está. Me pasó midiendo esto mismo.""" out = {} for d in glob.glob(os.path.join(RAIZ, "store", "*")): m = re.match(r"^[0-9a-f]{64}-(.+)$", os.path.basename(d)) if m and os.path.isdir(d): out.setdefault(m.group(1), []).append(d) return out def provee_binario(arts, nombre_bin, ocultar=()): """¿Algún artefacto sellado trae un ejecutable llamado `nombre_bin`?""" for receta, dirs in arts.items(): if receta in ocultar: continue for d in dirs: for sub in ("bin", "sbin", "usr/bin", "usr/sbin", "usr/local/bin"): p = os.path.join(d, sub, nombre_bin) if os.path.exists(p): return receta return None def barrido(arts, ocultar=()): """→ lista de (prefijo, binario, quien_deberia, [recetas inertes]).""" recetas = set() for patron in ("recipes/*.toml", "recipes/incoming*/*.toml"): for f in glob.glob(os.path.join(RAIZ, patron)): recetas.add(os.path.basename(f)[:-5]) hallazgos = [] for prefijo, (binario, quien) in sorted(DRIVERS.items()): subs = sorted(r for r in recetas if r.startswith(prefijo) and r != prefijo.rstrip("-")) if not subs: continue if provee_binario(arts, binario, ocultar) is None: hallazgos.append((prefijo, binario, quien, subs)) return hallazgos def autoprueba(): """Control NEGATIVO + control POSITIVO. Sin los dos, no se sabe si el vigía mide algo.""" arts = artefactos() ok = True # NEGATIVO: escondemos `git`, que SÍ está y tiene subcomandos ⇒ el vigía tiene que verlo. quien_git = provee_binario(arts, "git") if quien_git is None: print(" ⚠ control negativo NO APLICABLE: `git` no está sellado en esta máquina") else: roto = [p for p, _b, _q, _s in barrido(arts, ocultar={quien_git})] if "git-" in roto: print(f" ✓ control NEGATIVO: escondiendo `{quien_git}`, el vigía canta `git-` ") else: print(f" ✗ control NEGATIVO FALLÓ: escondí `{quien_git}` y el vigía no dijo nada") ok = False # POSITIVO: sin tocar nada, los drivers que SÍ están no deben aparecer. Un vigía que grita # sobre todo es igual de inútil que uno que nunca grita. presentes = [p for p, (b, _q) in DRIVERS.items() if provee_binario(arts, b)] ruidosos = [p for p, _b, _q, _s in barrido(arts) if p in presentes] if ruidosos: print(f" ✗ control POSITIVO FALLÓ: canta sobre drivers presentes: {ruidosos}") ok = False else: print(f" ✓ control POSITIVO: no canta sobre los {len(presentes)} drivers presentes " f"({', '.join(p.rstrip('-') for p in presentes)})") return ok def main(): if "--autoprueba" in sys.argv: print("══ AUTOPRUEBA DEL VIGÍA ══") sys.exit(0 if autoprueba() else 2) arts = artefactos() hallazgos = barrido(arts) print("══ SUBCOMANDOS SIN DRIVER ══") if not hallazgos: print(" ✓ 0 herramientas inertes: todos los drivers de la tabla están en el catálogo") sys.exit(0) total = nuevos = 0 for prefijo, binario, quien, subs in hallazgos: total += len(subs) conocido = CONOCIDOS.get(binario) marca = "•" if conocido else "✗" print(f"\n {marca} falta el binario `{binario}` ({quien})") print(f" ⇒ {len(subs)} recetas selladas quedan INERTES:") for i in range(0, len(subs), 6): print(" " + " ".join(subs[i:i + 6])) if conocido: for linea in textwrap.wrap(conocido, 92): print(f" {linea}") else: nuevos += len(subs) print(f"\n TOTAL: {total} herramientas selladas que no se pueden invocar " f"({total - nuevos} de hueco CONOCIDO, {nuevos} NUEVAS).") print(" No es deuda de BUILD (construyen y reproducen): es deuda de CATÁLOGO.") sys.exit(1 if nuevos else 0) if __name__ == "__main__": main()