tawasuyu

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 1574Shell 204Rust 111Markdown 993763 Definiciones

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>

d0c9cdfa337b38a1631db9743a7b51f7d92f2736 · por sergio · (hace 3 meses) · padre 47982d7c6 · ver el árbol en este commit

Delta semántico · 3

scripts/harkaq/harkaq-suggest.py

4 archivos · +252 −0

diff --git a/docs/16-harkaq-jaula.md b/docs/16-harkaq-jaula.md
index bce7e86c..5b7bb304 100644
--- a/docs/16-harkaq-jaula.md
+++ b/docs/16-harkaq-jaula.md
@@ -719,6 +719,64 @@ qué usó sin declarar, con path/dev/ino.
 > `/etc/passwd`. Una receta real no de-Alpinizada es *mejor demo* (es el problema que de verdad
 > tenemos) y de paso le entrega valor inmediato a una campaña en curso.
 
+### 4.5 Deuda *declarable* vs *irreducible*: la métrica del <5% 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`. O sea que a zlib **le falta una línea en `[deps]`**, no una excepción.
+
+Esa distinción rehace la métrica del §7, que hablaba de "recetas que necesitan excepción":
+
+| | Qué | Cuenta contra el <5% |
+|---|---|---|
+| **declarable** | el store ya provee el path ⇒ una línea en `[deps]`, arreglo mecánico | **no** |
+| **irreducible** | nadie lo provee ⇒ escribir receta, o aceptar como runtime base | **sí** |
+
+`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) y lo dice:
+
+```
+DEUDA DECLARABLE — el store ya lo provee; falta la línea en [deps]:
+    /usr/bin/make          → declarar dep: make
+DEUDA IRREDUCIBLE — nadie la provee:
+    /usr/lib/libz.so.1.3.2
+```
+
+Las dos son correctas y la segunda es sutil: la receta de zlib de hammer produce `libz.a`
+estático, no un `.so`, así que un consumidor que quiera el shared **no** se arregla declarando.
+El diagnóstico deja de ser "algo pasó" y pasa a ser "hacé esto".
+
+### 4.6 Primer barrido de Fase 2 (6 recetas)
+
+`scripts/harkaq/fase2-barrido.sh`. Muestra chica y deliberadamente fácil (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) |
+
+**El resultado que importa:** la deuda de 5 de 6 recetas es **un solo path**, y se arregla con una
+línea. No hay una 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 la frontera que la campaña *matar gcc* ya tenía identificada
+("gcc retenido sólo para {kernel, cmake}", "lo que queda: los `*-sys` con C++"). **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 sobre la muestra: 6 recetas no son 750, y son las fáciles. 1/6 irreducible es 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, no de diseño.
+
+**Bug del barrido, encontrado por el propio barrido:** copiar la receta a otro directorio rompe la
+resolución de `deps.build` (son relativas al directorio de la receta) ⇒ brotli fallaba con "no
+pude cargar la dep 'cmake'" y se descartaba en silencio. Se descartaban **justo las recetas con
+deps, que son las interesantes**. La copia va al lado del original.
+
+---
+
 **Fase 2 — `harkaq-policy` completo + medir la brecha.** Clausura real, `/src`, `/out`, `/tmp`,
 `/cache`, `/proc` y `/dev` mínimos, y el **runtime base** (sin `/bin/sh` ni la libc del rootfs no
 arranca nada — ver Q3). Correr contra N recetas del catálogo y medir la tasa de falsos positivos.
diff --git a/scripts/harkaq/README.md b/scripts/harkaq/README.md
index 09c6c3ab..f7b46cdf 100644
--- a/scripts/harkaq/README.md
+++ b/scripts/harkaq/README.md
@@ -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.
diff --git a/scripts/harkaq/fase2-barrido.sh b/scripts/harkaq/fase2-barrido.sh
new file mode 100755
index 00000000..58ed6086
--- /dev/null
+++ b/scripts/harkaq/fase2-barrido.sh
@@ -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/^/    /'
diff --git a/scripts/harkaq/harkaq-suggest.py b/scripts/harkaq/harkaq-suggest.py
new file mode 100755
index 00000000..f95d8459
--- /dev/null
+++ b/scripts/harkaq/harkaq-suggest.py
@@ -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())