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 1389Shell 137Markdown 80Rust 802602 Definiciones

⎇ c202d4c1d0935c9abfb91ea79c02462ede13252a scripts/vigia-subcomandos.py Python · 179 líneas · 9.1 KB Historia Quién tocó qué crudo

vigía de subcomandos: separar el hueco CONOCIDO del NUEVO — «44 inertes» todos los días no se lee df1230d2a · Sergio ·

1#!/usr/bin/env python3
2# vigia-subcomandos.py — ¿hay herramientas SELLADAS que no sirven porque falta su DRIVER?
3#
4# ══ EL PUNTO CIEGO QUE ESTE VIGÍA CUBRE ════════════════════════════════════════════════════════
5# `build-state.py` contesta «¿está sellado?», `vigia-sonames.py` contesta «¿algún `.so` que alguien
6# pide no lo provee nadie?» y `vigia-imagen.py` contesta «¿la imagen se puede USAR?». Ninguno
7# contesta ésta: **¿este binario sellado sirve para algo por sí solo?**
8#
9# Hay una familia entera de herramientas que NO son programas: son SUBCOMANDOS. `protoc-gen-go` es un
10# plugin que `protoc` ejecuta; `cargo-audit` existe para escribirse `cargo audit`; `kubectl-tree` lo
11# despacha `kubectl`. Sellados, no vacíos, sin NEEDED colgante, reproducibles — y **inertes**, porque
12# el driver que los invoca no está en el catálogo.
13#
14# ⚠ **Ningún indicador existente puede verlo.** El artefacto tiene contenido (regla 3 ✓), resuelve
15# sus sonames (vigia-sonames ✓), el grafo lo cuenta como `sealed` (✓) y `verificar-repro` dice que
16# REPRODUCE (✓). Cuatro verdes sobre algo que no se puede usar. La arista «me ejecuta aquél» no es
17# una dep de build, así que no existe en el grafo: se descubre sólo cuando alguien va a usarlo.
18#
19# ══ CÓMO LO ENCONTRÉ (2026-09-11), que es la razón de que este fichero exista ═══════════════════
20# `recipes/protoc-gen-go.toml` llevaba meses sellado y el catálogo NO tenía `protoc`. Un plugin de
21# protoc sin protoc no hace absolutamente nada. No lo delató ninguna métrica: lo delató ir a escribir
22# la receta de un servicio gRPC y preguntarse con qué se generan los stubs.
23#
24# ══ QUÉ MIDE, Y POR QUÉ ASÍ ════════════════════════════════════════════════════════════════════
25# Para cada prefijo de la tabla DRIVERS, junta las recetas que empiezan con él y comprueba que el
26# **BINARIO** del driver exista dentro de algún artefacto sellado.
27#
28#   · Se comprueba el BINARIO y no el nombre de la receta a propósito: el driver de `protoc-gen-*` es
29#     `protoc`, que lo publica la receta `protobuf`. Preguntar «¿existe recipes/protoc.toml?» habría
30#     contestado «no» incluso después de cerrarlo, y un vigía que grita cuando ya está arreglado se
31#     empieza a ignorar — que es cómo se muere un guardián ([[guardianes-el-ruido-los-mata]]).
32#   · La tabla es EXPLÍCITA y no una heurística sobre guiones. `git-cliff` es subcomando de git, pero
33#     `gettext-tiny` no es subcomando de `gettext`. Adivinar por el guión da falsos positivos, y un
34#     vigía con falsos positivos no se lee.
35#
36# ══ AUTOPRUEBA ═════════════════════════════════════════════════════════════════════════════════
37# `--autoprueba` corre los dos controles que hacen falta para creerle a un guardián: uno NEGATIVO
38# (esconder un driver que sí está ⇒ TIENE que saltar) y uno POSITIVO (sin tocar nada, los drivers
39# presentes NO deben aparecer). Un guardián que nunca falló no se sabe si sirve.
40#
41# Uso:   scripts/vigia-subcomandos.py [--autoprueba]
42# Sale:  0 si no hay inertes · 1 si los hay · 2 si la autoprueba falla
43import glob
44import os
45import textwrap
46import re
47import sys
48
49RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
50
51# prefijo de receta → (binario del driver, quién se supone que lo publica, para el mensaje)
52DRIVERS = {
53    "cargo-": ("cargo", "el toolchain de Rust"),
54    "kubectl-": ("kubectl", "una receta kubectl"),
55    "git-": ("git", "recipes/git.toml"),
56    "helm-": ("helm", "recipes/helm.toml"),
57    "protoc-gen-": ("protoc", "recipes/protobuf.toml"),
58    "docker-": ("docker", "una receta docker"),
59}
60
61
62# Huecos ya CONOCIDOS y con su motivo, para que el fichero diario no diga «44 inertes» a secas todos
63# los días. Un número grande y constante, sin explicación al lado, entrena a saltearse el informe —
64# y entonces el hueco NUEVO, que es el que importa, pasa desapercibido entre el ruido del viejo.
65# Se imprimen como `•` y NO cuentan para el código de salida.
66CONOCIDOS = {
67    "cargo": "PENDIENTE DE DECISIÓN, no olvido. `cargo`/`rustc` viven hoy en el LAB y en la cadena "
68             "de selfhost (mrustc→1.91.1), no como receta del catálogo. Empaquetarlos es una "
69             "decisión de TOOLCHAIN —qué Rust publica la distro, y desde qué cadena— no trabajo de "
70             "granja. Hasta que se tome, estos 44 son catálogo sin consumidor.",
71}
72
73
74def artefactos():
75    """Todos los directorios de artefacto del store, por nombre de receta.
76
77    ⚠ El nombre se saca con una EXPRESIÓN ANCLADA, no con `glob('store/*-<n>')`: ese glob hace
78    coincidencia por sufijo, así que `*-go` casa con `…-protoc-gen-go` y contesta que `go` está
79    sellado cuando no lo está. Me pasó midiendo esto mismo."""
80    out = {}
81    for d in glob.glob(os.path.join(RAIZ, "store", "*")):
82        m = re.match(r"^[0-9a-f]{64}-(.+)$", os.path.basename(d))
83        if m and os.path.isdir(d):
84            out.setdefault(m.group(1), []).append(d)
85    return out
86
87
88def provee_binario(arts, nombre_bin, ocultar=()):
89    """¿Algún artefacto sellado trae un ejecutable llamado `nombre_bin`?"""
90    for receta, dirs in arts.items():
91        if receta in ocultar:
92            continue
93        for d in dirs:
94            for sub in ("bin", "sbin", "usr/bin", "usr/sbin", "usr/local/bin"):
95                p = os.path.join(d, sub, nombre_bin)
96                if os.path.exists(p):
97                    return receta
98    return None
99
100
101def barrido(arts, ocultar=()):
102    """→ lista de (prefijo, binario, quien_deberia, [recetas inertes])."""
103    recetas = set()
104    for patron in ("recipes/*.toml", "recipes/incoming*/*.toml"):
105        for f in glob.glob(os.path.join(RAIZ, patron)):
106            recetas.add(os.path.basename(f)[:-5])
107    hallazgos = []
108    for prefijo, (binario, quien) in sorted(DRIVERS.items()):
109        subs = sorted(r for r in recetas if r.startswith(prefijo) and r != prefijo.rstrip("-"))
110        if not subs:
111            continue
112        if provee_binario(arts, binario, ocultar) is None:
113            hallazgos.append((prefijo, binario, quien, subs))
114    return hallazgos
115
116
117def autoprueba():
118    """Control NEGATIVO + control POSITIVO. Sin los dos, no se sabe si el vigía mide algo."""
119    arts = artefactos()
120    ok = True
121
122    # NEGATIVO: escondemos `git`, que SÍ está y tiene subcomandos ⇒ el vigía tiene que verlo.
123    quien_git = provee_binario(arts, "git")
124    if quien_git is None:
125        print("   ⚠ control negativo NO APLICABLE: `git` no está sellado en esta máquina")
126    else:
127        roto = [p for p, _b, _q, _s in barrido(arts, ocultar={quien_git})]
128        if "git-" in roto:
129            print(f"   ✓ control NEGATIVO: escondiendo `{quien_git}`, el vigía canta `git-` ")
130        else:
131            print(f"   ✗ control NEGATIVO FALLÓ: escondí `{quien_git}` y el vigía no dijo nada")
132            ok = False
133
134    # POSITIVO: sin tocar nada, los drivers que SÍ están no deben aparecer. Un vigía que grita
135    # sobre todo es igual de inútil que uno que nunca grita.
136    presentes = [p for p, (b, _q) in DRIVERS.items() if provee_binario(arts, b)]
137    ruidosos = [p for p, _b, _q, _s in barrido(arts) if p in presentes]
138    if ruidosos:
139        print(f"   ✗ control POSITIVO FALLÓ: canta sobre drivers presentes: {ruidosos}")
140        ok = False
141    else:
142        print(f"   ✓ control POSITIVO: no canta sobre los {len(presentes)} drivers presentes "
143              f"({', '.join(p.rstrip('-') for p in presentes)})")
144    return ok
145
146
147def main():
148    if "--autoprueba" in sys.argv:
149        print("══ AUTOPRUEBA DEL VIGÍA ══")
150        sys.exit(0 if autoprueba() else 2)
151
152    arts = artefactos()
153    hallazgos = barrido(arts)
154    print("══ SUBCOMANDOS SIN DRIVER ══")
155    if not hallazgos:
156        print("   ✓ 0 herramientas inertes: todos los drivers de la tabla están en el catálogo")
157        sys.exit(0)
158    total = nuevos = 0
159    for prefijo, binario, quien, subs in hallazgos:
160        total += len(subs)
161        conocido = CONOCIDOS.get(binario)
162        marca = "•" if conocido else "✗"
163        print(f"\n   {marca} falta el binario `{binario}` ({quien})")
164        print(f"     ⇒ {len(subs)} recetas selladas quedan INERTES:")
165        for i in range(0, len(subs), 6):
166            print("       " + "  ".join(subs[i:i + 6]))
167        if conocido:
168            for linea in textwrap.wrap(conocido, 92):
169                print(f"     {linea}")
170        else:
171            nuevos += len(subs)
172    print(f"\n   TOTAL: {total} herramientas selladas que no se pueden invocar "
173          f"({total - nuevos} de hueco CONOCIDO, {nuevos} NUEVAS).")
174    print("   No es deuda de BUILD (construyen y reproducen): es deuda de CATÁLOGO.")
175    sys.exit(1 if nuevos else 0)
176
177
178if __name__ == "__main__":
179    main()