Files
takana/scripts/vigia-subcomandos.py
Sergio df1230d2a5 vigía de subcomandos: separar el hueco CONOCIDO del NUEVO — «44 inertes» todos los días no se lee
El vigía escribe `docs/state/subcomandos.txt` en cada ciclo del latido desde ayer, y decía
`TOTAL: 44 herramientas selladas que no se pueden invocar` a secas. Un número grande, constante y sin
explicación al lado entrena a saltearse el informe — y entonces el hueco NUEVO, que es el único que
pide acción, pasa desapercibido entre el ruido del viejo. Es la misma trampa que ya tienen resuelta
`static-audit.sh` (deuda decidida) y `--auditar-raices`.

Los 44 son todos del mismo hueco, y ahora el informe dice cuál y por qué:

    • falta el binario `cargo` (el toolchain de Rust)
      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.

    TOTAL: 44 … (44 de hueco CONOCIDO, 0 NUEVAS)   ⇒ exit 0

⚠ **«PENDIENTE DE DECISIÓN» y no «deuda decidida»**, que es distinto: en el audit estático las tres
mentiras tienen un arreglo evaluado y descartado por su coste; acá nadie decidió nada todavía. La
etiqueta tiene que decir cuál de las dos cosas es, o dentro de tres meses se lee como cerrada.

El código de salida ahora refleja sólo los huecos NUEVOS. Probado con los dos controles: sacando
`cargo` de la tabla salen `44 NUEVAS` y exit 1; restaurándola, `0 NUEVAS` y exit 0.

⚠ Y el primer intento de ese control estaba MAL y daba verde: escribí `CONOCIDOS = {} or {…}` para
vaciar la tabla, y en Python `{}` es falsy ⇒ la expresión devuelve el segundo dict y la tabla seguía
llena. El control «pasaba» sin probar nada. Rehecho renombrando la clave.
2026-09-13 20:02:32 +00:00

180 lines
9.1 KiB
Python
Executable File

#!/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/*-<n>')`: 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()