Files
Sergio 476168bb07 takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca
NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa.

El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces:
las tres eran el MOTD que el script escribe DENTRO de la imagen construida —
texto del producto, no comentario del script. Se cambiaron aparte y a
propósito, que es rebranding, no limpieza.

Y el hallazgo caro:  casaba contra ,
que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate.
La etapa 4 lo movió a  y el script quedó casando NADA. No fallaba:
imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado
con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y
tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja.

Además 14 rutas de módulo  en docs, que el barrido anterior no tocó
porque  no es frontera de palabra.
2026-09-09 19:28:48 +00:00

185 lines
8.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""harkaq-suggest — convierte la deuda en una acción concreta (SDD 16 §4.5).
harkaq-suggest.py <verdict-crudo.json> <politica> <store> [recipes-dir]
Un veredicto `Impuro` dice *qué* usó el build sin declarar. Esto dice *qué hacer*: por cada path
de deuda busca en el store un artefacto sellado que provea ese mismo path relativo. Si lo hay, la
deuda es **declarable** — la receta sólo tiene que añadir la dep, y hammer ya sabe construirla.
La distinción importa para la métrica de la Fase 2 (<5% de recetas necesitan excepción):
declarable el store ya lo provee ⇒ NO es una excepción, es una línea en `[deps]`. El
diagnóstico es accionable y el arreglo, mecánico.
irreducible nadie lo provee ⇒ o se escribe una receta, o se acepta como runtime base. Ésta
es la que cuenta contra el <5%.
Ejemplo real: zlib salió `Impuro` por `/usr/bin/make`. Parecía runtime base — pero
`recipes/make.toml` existe y `store/…-make` está sellado, y `SWAP_MAKE` es uno de los swaps del
selfhost-verify. O sea: no es base, es deuda declarable. La diferencia entre "aceptamos que
hammer usa el make de Alpine" y "a zlib le falta una línea" la decide este cruce, no una opinión.
"""
import json
import os
import sys
def indice_del_store(store):
"""path-dentro-del-sandbox → [artefactos que lo proveen].
`--overlay-src <dep>` funde `<dep>/usr/...` en `/usr/...`, así que el path relativo dentro
del artefacto ES el path del sandbox. El mismo mapeo que usa harkaq-policy, al revés.
"""
idx = {}
for art in sorted(os.listdir(store)):
raiz = os.path.join(store, art)
if not os.path.isdir(raiz):
continue
for dirpath, _, files in os.walk(raiz):
if "/.hammer" in dirpath:
continue
for f in files:
completo = os.path.join(dirpath, f)
dentro = "/" + os.path.relpath(completo, raiz)
idx.setdefault(dentro, []).append(art)
return idx
def nombre_de(art):
"""`<64hex>-<name>` → `<name>`."""
return art.split("-", 1)[1] if "-" in art else art
def catalogo(recipes_dir):
"""Nombres de receta que hammer SABE construir, tenga o no el artefacto a mano.
Sin esto, "declarable" sería relativo a la máquina: el store del worker de la granja trae 103
artefactos y el del laptop cientos, así que el MISMO path (`/usr/bin/make`) salía declarable
en uno e irreducible en el otro. Eso no es una propiedad de la receta: es del disco donde
corrés el barrido, y habría inflado la métrica de la Fase 2 en la granja — que es justo donde
se va a medir en serio.
El catálogo es la respuesta correcta a "¿esto lo sabemos construir?". El store sólo dice
"¿lo tenemos construido AHORA?", que es otra pregunta.
"""
if not recipes_dir or not os.path.isdir(recipes_dir):
return set()
return {f[:-5] for f in os.listdir(recipes_dir) if f.endswith(".toml")}
# Los frontends del gcc de Alpine. Son "sonda esperada" (D3) SÓLO cuando la receta compila con
# zig (el default): el configure los sondea y zig no los usa. Pero para una receta `compiler=gcc`
# (`CC=gcc`, el gcc de Alpine), gcc ES el compilador y su uso es DEUDA DE COMPILADOR — el frente
# matar-gcc. Sin esta distinción, harkaq marcaba `Hermetico` recetas que dependen del gcc de
# Alpine (47 de ellas), ciego a la deuda mayor. SDD 16, límite del modelo (2026-07-16).
FRONTENDS_GCC = {"/usr/bin/gcc", "/usr/bin/cpp", "/usr/bin/c89", "/usr/bin/c99", "/usr/bin/g++",
"/usr/bin/cc", "/usr/bin/ld", "/usr/bin/as"}
def compiler_de(recipe_toml):
"""`gcc` si la receta usa el gcc de Alpine como compilador, si no `zig`."""
try:
s = open(recipe_toml).read()
except OSError:
return "zig"
import re
m = re.search(r'compiler\s*=\s*"([^"]+)"', s)
return "gcc" if m and m.group(1) == "gcc" else "zig"
def main():
if len(sys.argv) < 4:
print(__doc__.split("\n")[2].strip(), file=sys.stderr)
return 2
crudo = json.load(open(sys.argv[1]))
politica, store = sys.argv[2], sys.argv[3]
recipes_dir = sys.argv[4] if len(sys.argv) > 4 else os.path.join(
os.path.dirname(os.path.abspath(store)), "recipes")
# 5º arg opcional: la receta que se midió. Si usa compiler=gcc, los frontends de gcc dejan de
# ser "esperada" y pasan a DEUDA DE COMPILADOR (matar-gcc).
receta_toml = sys.argv[5] if len(sys.argv) > 5 else None
usa_gcc = receta_toml and compiler_de(receta_toml) == "gcc"
esperadas = set()
# RUNTIME BASE: lo que la política ya CONCEDE (`ro`/`rw`/`list`). Si un path está acá, NO es
# deuda declarable aunque el store lo provea — es contrato del sandbox.
#
# Este bug costó caro: `/bin/busybox` está concedido (el sandbox corre `sh -c` y /bin/sh→
# busybox), pero como `recipes/busybox.toml` existe, el clasificador propuso "declarar dep:
# busybox" y la cosecha lo declaró en 29 recetas. Declararlo apila el busybox de takana sobre
# el de Alpine y ROMPE el build (binutils: "cannot run C compiled programs"). Además va contra
# la Etapa C, que reemplaza busybox por uutils. Medir bien y accionar mal.
concedidos = set()
with open(politica) as f:
for l in f:
l = l.strip()
if l.startswith("# expect "):
esperadas.add(l[len("# expect "):].strip())
elif l[:3] in ("ro ", "rw ") or l.startswith("list "):
concedidos.add(l.split(None, 1)[1].strip())
# Si la receta compila con gcc, los frontends NO se perdonan: son la deuda de compilador.
if usa_gcc:
esperadas -= FRONTENDS_GCC
deuda = sorted({d["path"] for d in crudo.get("denials", []) if d["path"] not in esperadas})
if not deuda:
print("sin deuda ✓")
return 0
idx = indice_del_store(store)
recetas = catalogo(recipes_dir)
declarable, irreducible, compilador, base = {}, [], [], []
for p in deuda:
if usa_gcc and p in FRONTENDS_GCC:
compilador.append(p)
continue
# ¿Ya concedido por la política? ⇒ runtime base, NO se declara (ver arriba: busybox).
if p in concedidos or any(p.startswith(c.rstrip("/") + "/") for c in concedidos if c != "/"):
base.append(p)
continue
provs = idx.get(p)
if provs:
# El store es autoritativo: sabe qué ficheros aporta cada artefacto.
declarable[p] = sorted({nombre_de(a) for a in provs})
elif os.path.basename(p) in recetas:
# Respaldo por catálogo: el artefacto no está construido AQUÍ, pero takana sabe
# construirlo. Es declarable igual — la deuda es de la receta, no del disco.
#
# OJO, heurística por NOMBRE y por eso sólo un respaldo: acierta con `make`, pero
# falla con `/usr/bin/ranlib` (lo trae `binutils`) y `/usr/bin/diff` (lo trae
# `diffutils`). Un fichero no se llama como su paquete. El único que sabe la verdad
# es el store, porque conoce la lista de ficheros de cada artefacto.
#
# ⇒ CLASIFICAR EN EL HUB, no en el worker. El worker de la granja mide (produce los
# veredictos crudos) y el hub clasifica, porque es quien tiene el store COMPLETO. Con
# el store parcial del worker (103 artefactos) esto marcaba bash y doas como deuda
# IRREDUCIBLE y habría dado una métrica de Fase 2 inflada. Misma separación que
# lector/clasificador: el que tiene el privilegio (o los datos) hace lo mínimo.
declarable[p] = [os.path.basename(p) + " (receta en el catálogo, sin construir aquí)"]
else:
irreducible.append(p)
if base:
print("RUNTIME BASE — ya concedido por la política; NO declarar (es contrato del sandbox):")
for p in base:
print(f" {p}")
if compilador:
print("DEUDA DE COMPILADOR (matar-gcc) — la receta usa el gcc de Alpine (compiler=gcc):")
for p in compilador:
print(f" {p} → migrar a zig-cc, o aceptar como escape declarativo documentado")
if declarable:
print("DEUDA DECLARABLE — el store ya lo provee; falta la línea en [deps]:")
for p, quien in declarable.items():
print(f" {p}\n → declarar dep: {', '.join(quien)}")
if irreducible:
print("DEUDA IRREDUCIBLE — nadie la provee (escribir receta, o aceptar como runtime base):")
for p in irreducible:
print(f" {p}")
# El exit code separa "arreglable con una línea" de "hace falta trabajo de verdad": es lo que
# un barrido de la Fase 2 necesita para contar contra el <5% sin leer texto.
return 0 if not irreducible else 1
if __name__ == "__main__":
sys.exit(main())