harkaq: deuda declarable vs irreducible (§4.5) + primer barrido de Fase 2 (§4.6)
La métrica del §7 (<5% de recetas necesitan excepción) medía otra cosa. zlib
salió Impuro por /usr/bin/make y parecía runtime base: NO lo es. recipes/make.toml
existe, store/fbad44ac…-make está sellado y SWAP_MAKE es uno de los swaps del
selfhost-verify ⇒ a zlib le falta una LÍNEA en [deps], no una excepción.
declarable el store ya provee el path ⇒ arreglo mecánico → NO cuenta
irreducible nadie lo provee ⇒ receta nueva o runtime base → SÍ cuenta
harkaq-suggest.py cruza cada path de deuda contra el store (el mapeo de §4.1 al
revés: el path relativo dentro del artefacto ES el path del sandbox):
/usr/bin/make → declarar dep: make
/usr/lib/libz.so.1.3.2 → irreducible (la zlib de hammer da libz.a estático,
no .so ⇒ NO se arregla declarando)
El diagnóstico deja de ser "algo pasó" y pasa a ser "hacé esto".
PRIMER BARRIDO (6 recetas C, fuentes cacheadas):
Hermetico ............... 0
sólo deuda DECLARABLE ... 5 ← y la deuda es EL MISMO path en las 5: /usr/bin/make
con deuda IRREDUCIBLE ... 1 (brotli)
Lo que importa: la deuda de 5/6 es UN SOLO path y se arregla con una línea. No
hay cola larga de excepciones — que era el riesgo de muerte del §7.
Y el caso irreducible es la frontera CONOCIDA: brotli (C++) pide
/usr/lib/libstdc++.so.6.0.34 y /usr/lib/libgcc_s.so.1 — el runtime C++ del gcc de
Alpine, que hammer no construye. Es exactamente lo que la campaña "matar gcc" ya
tenía identificado. TERCERA vez que harkaq llega por su cuenta a una lista que
otro frente ya tenía: los swaps del selfhost-verify (§4.3), los binutils (§4.4)
y ahora el runtime C++.
Honestidad: 6 recetas no son 750 y son las fáciles. 1/6 = 17%, muy por encima del
<5% — pero el único caso es un problema conocido, nombrado y compartido con otros
dos frentes, no una cola de sorpresas. El barrido grande es trabajo de granja.
Bug del barrido encontrado por el propio barrido: copiar la receta a otro
directorio rompe la resolución de deps.build (son relativas al dir de la receta)
⇒ brotli fallaba con "no pude cargar la dep 'cmake'" y se descartaba EN SILENCIO
— o sea que se descartaban justo las recetas CON deps, las interesantes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -266,3 +266,24 @@ DEUDA (34) en 8 paths:
|
||||
Sin clasificar son 205 líneas planas; clasificado, 8 paths accionables. **Regla que salió de acá:
|
||||
todo directorio concedido necesita `list` en sus ancestros**, o el escaneo del padre es un falso
|
||||
positivo.
|
||||
|
||||
## Fase 2 — el barrido
|
||||
|
||||
`fase2-barrido.sh <receta.toml>...` + `harkaq-suggest.py`. La métrica del SDD (<5% de recetas
|
||||
necesitan excepción) medía otra cosa: una deuda que el store YA PROVEE no es una excepción, es
|
||||
una línea en `[deps]`. Sólo la deuda **irreducible** cuenta.
|
||||
|
||||
```sh
|
||||
scripts/harkaq/harkaq-base-closure.py .dev-fs/alpine /bin/sh /bin/busybox /bin/bash /bin/coreutils /usr/bin/env > ~/.cache/harkaq/base.policy
|
||||
printf "ro /usr/lib/os-release\n# expect /usr/bin/gcc\n# expect /usr/bin/ldd\n" >> ~/.cache/harkaq/base.policy
|
||||
scripts/harkaq/fase2-barrido.sh recipes/zlib.toml recipes/brotli.toml ...
|
||||
```
|
||||
|
||||
Primer barrido (6 recetas C, fuentes cacheadas): **5 con sólo deuda declarable —y las 5 por el
|
||||
MISMO path, `/usr/bin/make`—** y 1 irreducible (brotli: `libstdc++`/`libgcc_s`, el runtime C++ de
|
||||
Alpine = la frontera que *matar gcc* ya tenía identificada). No hay cola larga de excepciones,
|
||||
que era el riesgo de muerte del §7.
|
||||
|
||||
**Gotcha del barrido:** copiar la receta a otro directorio rompe la resolución de `deps.build`
|
||||
(son relativas al dir de la receta) ⇒ se descartaban en silencio justo las recetas CON deps. La
|
||||
copia va al lado del original.
|
||||
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
# Fase 2 — barrer el catálogo y medir la tasa de excepciones (SDD 16 §6).
|
||||
#
|
||||
# fase2-barrido.sh <receta.toml> [<receta.toml>...]
|
||||
#
|
||||
# El criterio de viabilidad del SDD (§7): "si el 30% de los builds necesita excepción manual,
|
||||
# harkaq se desactiva y queda como decoración". Métrica: <5%.
|
||||
#
|
||||
# Pero "excepción" no es "denegación" (§4.5). Una deuda que el store YA PROVEE no es una
|
||||
# excepción: es una línea en `[deps]` que falta, y el arreglo es mecánico. Sólo la deuda
|
||||
# IRREDUCIBLE —nadie la provee— cuenta contra el <5%. Sin esa distinción la métrica mide
|
||||
# ansiedad, no viabilidad.
|
||||
#
|
||||
# `hammer build` pega en la caché si el artefacto ya está sellado (no correría nada y el
|
||||
# veredicto saldría vacío sin que sea un fallo). Se fuerza un build real copiando la receta con
|
||||
# otro `name` — cambia hash_inputs sin tocar el store real ni la receta del repo.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
BIN="${HARKAQ_BIN:-$HOME/.cache/harkaq}"
|
||||
BASE="${HARKAQ_BASE:-$BIN/base.policy}"
|
||||
OUT="${OUT:-work/harkaq-fase2}"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
export HARKAQ=1 HARKAQ_BIN="$BIN" HARKAQ_BASE="$BASE"
|
||||
export HARKAQ_TIMEOUT="${HARKAQ_TIMEOUT:-120}"
|
||||
|
||||
total=0; limpias=0; solo_declarable=0; con_irreducible=0
|
||||
: > "$OUT/deuda-cruda.txt"
|
||||
|
||||
for receta in "$@"; do
|
||||
n=$(basename "$receta" .toml)
|
||||
total=$((total + 1))
|
||||
# La copia va AL LADO del original: las `deps.build` se resuelven relativas al directorio
|
||||
# de la receta, así que copiarla a otro sitio rompe `cmake.toml` y compañía — y descartaba
|
||||
# en silencio justo las recetas CON deps, que son las interesantes. Se limpia al salir.
|
||||
tmp="$(dirname "$receta")/.harkaq-$n.toml"
|
||||
sed "s/^name *= *\"$n\"/name = \"$n-hk\"/" "$receta" > "$tmp"
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
log="$OUT/$n.log"
|
||||
timeout "${POR_RECETA:-300}" ./target/debug/hammer build "$tmp" --store "$PWD/store" \
|
||||
> "$log" 2>&1 || true
|
||||
|
||||
# Un veredicto por fase (configure/compile/install son bwraps distintos ⇒ dominios distintos).
|
||||
rm -f "$tmp"
|
||||
grep '^\[harkaq\] {' "$log" | sed 's/^\[harkaq\] //' > "$OUT/$n.verdicts" || true
|
||||
if [ ! -s "$OUT/$n.verdicts" ]; then
|
||||
echo "?? $n — sin veredicto (¿caché? ¿fetch falló? ver $log)"
|
||||
continue
|
||||
fi
|
||||
|
||||
irr=0; dec=0
|
||||
while IFS= read -r v; do
|
||||
echo "$v" > "$OUT/.v.json"
|
||||
s=$(scripts/harkaq/harkaq-suggest.py "$OUT/.v.json" "$BASE" "$PWD/store" 2>/dev/null || true)
|
||||
echo "$s" | grep -q 'IRREDUCIBLE' && irr=1
|
||||
echo "$s" | grep -q 'DECLARABLE' && dec=1
|
||||
echo "$s" | sed -n 's/^ \(\/[^ ]*\)$/\1/p' >> "$OUT/deuda-cruda.txt"
|
||||
done < "$OUT/$n.verdicts"
|
||||
|
||||
if [ "$irr" = 1 ]; then
|
||||
con_irreducible=$((con_irreducible + 1)); echo "!! $n — deuda IRREDUCIBLE"
|
||||
elif [ "$dec" = 1 ]; then
|
||||
solo_declarable=$((solo_declarable + 1)); echo "~~ $n — sólo deuda declarable (falta [deps])"
|
||||
else
|
||||
limpias=$((limpias + 1)); echo "ok $n — Hermetico"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "════ FASE 2 — $total receta(s)"
|
||||
echo " Hermetico ................... $limpias"
|
||||
echo " sólo deuda DECLARABLE ....... $solo_declarable (arreglo mecánico: una línea en [deps])"
|
||||
echo " con deuda IRREDUCIBLE ....... $con_irreducible ← lo que cuenta contra el <5%"
|
||||
echo
|
||||
echo " paths de deuda más frecuentes:"
|
||||
sort "$OUT/deuda-cruda.txt" | uniq -c | sort -rn | head -12 | sed 's/^/ /'
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/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>
|
||||
|
||||
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 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]
|
||||
|
||||
esperadas = set()
|
||||
with open(politica) as f:
|
||||
for l in f:
|
||||
l = l.strip()
|
||||
if l.startswith("# expect "):
|
||||
esperadas.add(l[len("# expect "):].strip())
|
||||
|
||||
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)
|
||||
declarable, irreducible = {}, []
|
||||
for p in deuda:
|
||||
provs = idx.get(p)
|
||||
if provs:
|
||||
declarable[p] = sorted({nombre_de(a) for a in provs})
|
||||
else:
|
||||
irreducible.append(p)
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user