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 815Rust 56Shell 37Markdown 291229 Sutichaykuna

harkaq: campaña desatendida — medir→declarar→verificar, sin regex y sin preguntas

El problema real no es el coste de la tanda: es que una pregunta a las 3am son 8
horas de idle. Estas 3 piezas están hechas para que la noche no se desperdicie.

  harkaq-campana.sh   (worker) bucle autónomo: construye cada receta bajo la
                      jaula y guarda los veredictos crudos. Idempotente, saltea
                      lo medido, duerme y re-escanea. NUNCA bloquea esperando: un
                      build que falla no detiene la cola (y su veredicto interesa
                      igual — es cuando más importa saber qué tocó fuera). Gatea
                      por ABI>=7: moler la noche para producir "no sabemos" es
                      peor que no moler.
  harvest-harkaq.sh   (hub, cron) hermano de harvest-go.sh: baja veredictos,
                      clasifica contra el store COMPLETO, aplica las deps
                      DECLARABLES y commitea con `git add` explícito. La deuda
                      IRREDUCIBLE NO se toca (declarar algo que el store no
                      provee rompe el build en vez de arreglarlo) → needs-review.
  harkaq-add-deps.py  editor de recetas conservador: idempotente, preserva las
                      deps existentes, y ANTE LA DUDA NO TOCA (valida que el
                      resultado siga parseando como TOML y no encoja). Corre de
                      noche sobre la fuente de verdad del catálogo: un fichero mal
                      editado a las 3am no da un error, da una receta corrupta que
                      nadie mira hasta el lunes.

POR QUÉ NO HAY REGEX EN EL CAMINO CRÍTICO: intenté sacar la lista de "a quién le
falta declarar make" con uno y falló dos veces en la misma tarde. `\bmake\b`
colaba `cargo-make` (el guión ES límite de palabra). La versión estrecha perdía
LAS CINCO recetas donde harkaq había medido la deuda de verdad (`compile = "make
…"`: el carácter previo es una comilla). El número bailó 69→45→99 según el
retoque. Construí una herramienta de medición y después intenté adivinar con un
regex — la lista buena la da el kernel. El regex queda sólo para ORDENAR la cola.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

f783f0107dce3245c4168e3b3d21621df3cb5c83 · ruwaq sergio · (3 killa ñawpaq) · tayta 1e0bc98ad · kay commitpi sach'ata qhaway

Semántico sayk'uy · 2

scripts/farm/harkaq-add-deps.py

3 qillqakuna · +249 −0

diff --git a/scripts/farm/harkaq-add-deps.py b/scripts/farm/harkaq-add-deps.py
new file mode 100755
index 00000000..96ce52e2
--- /dev/null
+++ b/scripts/farm/harkaq-add-deps.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+"""harkaq-add-deps — añade deps de build a una receta, sin romperla. (Cosecha desatendida.)
+
+    harkaq-add-deps.py recipes/zlib.toml make binutils
+
+Idempotente: si la dep ya está, no toca nada y sale 1 (nada que commitear).
+
+Corre DESATENDIDO y de noche, editando ficheros que son la fuente de verdad del catálogo. Por eso
+la regla es: **ante la duda, no tocar**. Un fichero mal editado a las 3am no da un error, da una
+receta corrupta que nadie mira hasta el lunes. Cada caso que no se entiende sale sin cambios y con
+motivo, que es la misma disciplina de `SinEvidencia`: no afirmar lo que no se sabe.
+"""
+import re
+import sys
+
+
+def anadir(path, nuevas):
+    with open(path) as f:
+        s = f.read()
+    orig = s
+
+    m = re.search(r'^\[deps\]\s*$(.*?)(?=^\[|\Z)', s, re.M | re.S)
+    if m:
+        bloque = m.group(1)
+        mb = re.search(r'^(\s*build\s*=\s*)\[([^\]]*)\]', bloque, re.M)
+        if mb:
+            ya = re.findall(r'"([^"]+)"', mb.group(2))
+            falta = [d for d in nuevas if d not in ya]
+            if not falta:
+                return False, "ya estaban declaradas"
+            lista = ya + falta
+            nuevo = mb.group(1) + "[" + ", ".join(f'"{d}"' for d in lista) + "]"
+            s = s[:m.start(1)] + bloque[:mb.start()] + nuevo + bloque[mb.end():] + s[m.end(1):]
+        else:
+            # Hay [deps] pero sin `build = [...]` (p.ej. sólo `runtime`). Insertar la línea.
+            linea = "build = [" + ", ".join(f'"{d}"' for d in nuevas) + "]\n"
+            s = s[:m.start(1)] + "\n" + linea + bloque.lstrip("\n") + s[m.end(1):]
+    else:
+        # Sin [deps]: se añade al final. `[build.phases]` suele ser la última sección y sus strings
+        # multilínea (''' … ''') se comerían un bloque puesto después ⇒ va ANTES de la primera
+        # sección [build...] si existe, y si no, al final.
+        bloque = "\n[deps]\nbuild = [" + ", ".join(f'"{d}"' for d in nuevas) + "]\n"
+        mfase = re.search(r'^\[build\.phases\]', s, re.M)
+        if mfase:
+            s = s[:mfase.start()] + bloque.lstrip("\n") + "\n" + s[mfase.start():]
+        else:
+            s = s.rstrip("\n") + "\n" + bloque
+
+    if s == orig:
+        return False, "sin cambios"
+
+    # Guarda: sólo escribir si el TOML sigue pareciendo un TOML y no perdimos nada. Un parser de
+    # verdad sería mejor, pero tomllib no ESCRIBE y re-emitir con un writer reordenaría el fichero
+    # entero (y con él, comentarios que valen oro en este repo).
+    try:
+        import tomllib
+        tomllib.loads(s)
+    except Exception as e:
+        return False, f"el resultado no parsea como TOML ({e}) — NO se toca"
+    if len(s) < len(orig):
+        return False, "el resultado es más corto que el original — NO se toca"
+
+    with open(path, "w") as f:
+        f.write(s)
+    return True, "ok"
+
+
+def main():
+    if len(sys.argv) < 3:
+        print(__doc__.split("\n")[2].strip(), file=sys.stderr)
+        return 2
+    path, nuevas = sys.argv[1], sys.argv[2:]
+    ok, motivo = anadir(path, nuevas)
+    if not ok:
+        print(f"   ·· {path}: {motivo}", file=sys.stderr)
+    return 0 if ok else 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/scripts/farm/harkaq-campana.sh b/scripts/farm/harkaq-campana.sh
new file mode 100755
index 00000000..07d3d8b3
--- /dev/null
+++ b/scripts/farm/harkaq-campana.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+# harkaq-campana.sh — bucle AUTÓNOMO del worker para la campaña harkaq. Hermano de
+# farm-worker-loop.sh: muele sin esperar a nadie, 24/7, y no pregunta NADA.
+#
+#   scripts/farm/harkaq-campana.sh [lista-de-recetas.txt]
+#
+# Mide, no decide: por cada receta la construye bajo la jaula y guarda los veredictos crudos en
+# work/harkaq-campana/<n>.verdicts. La clasificación y la edición de recetas las hace el HUB
+# (harvest-harkaq.sh, por cron), porque el store del worker es parcial y marcaría como irreducible
+# lo que sí es declarable. El worker MIDE, el hub CLASIFICA.
+#
+# Idempotente: lo ya medido se saltea; re-correrlo sólo mide lo nuevo. Cuando no queda nada,
+# duerme y re-escanea — igual que farm-worker-loop.sh.
+#
+# Diseñado para la NOCHE: nunca bloquea esperando una decisión. Un build que falla NO detiene la
+# cola; su veredicto se guarda igual (un build que falla es justo cuando más interesa saber qué
+# tocó fuera de su clausura) y se sigue. Ocho horas de idle por una pregunta es el fallo que este
+# script existe para no tener.
+set -u
+
+HAMMER_DIR="${HAMMER_DIR:-/opt/hammer}"
+cd "$HAMMER_DIR"
+LISTA="${1:-work/harkaq-cola.txt}"
+OUT="work/harkaq-campana"
+BIN="${HARKAQ_BIN:-/root/.cache/harkaq}"
+IDLE_SLEEP="${IDLE_SLEEP:-300}"
+POR_RECETA="${POR_RECETA:-900}"
+mkdir -p "$OUT"
+
+export HARKAQ=1 HARKAQ_BIN="$BIN" HARKAQ_BASE="${HARKAQ_BASE:-$BIN/base.policy}"
+export HARKAQ_TIMEOUT="${HARKAQ_TIMEOUT:-300}"
+
+# Gate: sin ABI>=7 el kernel no audita y TODOS los veredictos saldrían SinEvidencia. Moler la
+# noche entera para producir "no sabemos" es peor que no moler.
+abi=$(python3 -c 'import ctypes;print(ctypes.CDLL("libc.so.6").syscall(444,None,0,1))' 2>/dev/null)
+if [ "${abi:-0}" -lt 7 ]; then
+    echo "ABORTO: Landlock ABI ${abi:-?} < 7 ⇒ sin audit de denegaciones. ¿La golden es la 6.17?"
+    exit 4
+fi
+echo "$(date -u +%FT%TZ) campaña harkaq: ABI $abi, cola $LISTA"
+
+while :; do
+    hechas=0; nuevas=0
+    while IFS= read -r n; do
+        [ -n "$n" ] || continue
+        [ -f "recipes/$n.toml" ] || continue
+        if [ -s "$OUT/$n.verdicts" ]; then hechas=$((hechas + 1)); continue; fi
+
+        # Forzar build real: `hammer build` pega en la caché si el artefacto ya está sellado y no
+        # correría nada (veredicto vacío que NO es un fallo). La copia va AL LADO del original o se
+        # rompe la resolución de deps.build, que son relativas al dir de la receta.
+        tmp="recipes/.hk-$n.toml"
+        sed "s/^name *= *\"$n\"/name = \"$n-hkm\"/" "recipes/$n.toml" > "$tmp"
+        timeout "$POR_RECETA" ./target/release/hammer build "$tmp" --store "$PWD/store" \
+            > "$OUT/$n.log" 2>&1
+        rm -f "$tmp"
+        grep '^\[harkaq\] {' "$OUT/$n.log" | sed 's/^\[harkaq\] //' > "$OUT/$n.verdicts"
+        if [ -s "$OUT/$n.verdicts" ]; then
+            nuevas=$((nuevas + 1))
+            echo "$(date -u +%FT%TZ) ++ $n ($(wc -l < "$OUT/$n.verdicts") fase/s medidas)"
+        else
+            # Sin veredicto: el build ni arrancó (fetch, dep rota…). Se marca para no reintentar
+            # en bucle y se SIGUE — la cola no se detiene por una receta.
+            echo "sin-veredicto" > "$OUT/$n.skip"
+            rm -f "$OUT/$n.verdicts"
+            echo "$(date -u +%FT%TZ) ?? $n — sin veredicto, salteada (ver $OUT/$n.log)"
+        fi
+    done < "$LISTA"
+
+    echo "$(date -u +%FT%TZ) ciclo: $nuevas nuevas, $hechas ya medidas. durmiendo ${IDLE_SLEEP}s"
+    sleep "$IDLE_SLEEP"
+done
diff --git a/scripts/farm/harvest-harkaq.sh b/scripts/farm/harvest-harkaq.sh
new file mode 100755
index 00000000..1e25b7ff
--- /dev/null
+++ b/scripts/farm/harvest-harkaq.sh
@@ -0,0 +1,97 @@
+#!/bin/sh
+# harvest-harkaq.sh — COSECHA-ONLY de la campaña harkaq (modo desatendido, para cron en el hub).
+# Determinista, sin IA. Hermano de harvest-go.sh y con la misma disciplina: no elige, no diagnostica,
+# no compila en el hub; sólo convierte en commit lo que el worker YA midió.
+#
+# Uso (cron):  cd /home/sergio/hammer && scripts/farm/harvest-harkaq.sh root@<ip> >>work/harvest-harkaq.log 2>&1
+#
+# POR QUÉ EXISTE ASÍ: la lista de "recetas a las que les falta declarar make" NO se puede sacar de
+# un regex. Se intentó y falló dos veces en la misma tarde: `\bmake\b` colaba `cargo-make` (el guión
+# es límite de palabra) y la versión estrecha perdía LAS CINCO recetas donde harkaq había medido la
+# deuda de verdad (`compile = "make …"`: el carácter previo es una comilla). El número bailó 69→45→99
+# según el retoque. La fuente de verdad es la MEDICIÓN, no la conjetura — que es exactamente para lo
+# que existe harkaq. Este script sólo aplica lo que el kernel denunció.
+#
+# Ciclo (idempotente, seguro de re-correr):
+#   1. git pull --rebase (otro agente pushea al mismo main)
+#   2. baja los veredictos del worker
+#   3. por cada veredicto: harkaq-suggest contra el store COMPLETO del hub (el del worker es parcial
+#      y marcaría como irreducible lo que sí es declarable — el worker MIDE, el hub CLASIFICA)
+#   4. si la deuda es DECLARABLE ⇒ añade la dep a la receta. Si es IRREDUCIBLE ⇒ NO la toca: va a
+#      tandas/needs-review-harkaq/ con el motivo. Declarar algo que el store no provee rompería el
+#      build en vez de arreglarlo.
+#   5. commit + push con `git add` EXPLÍCITO (nunca -A: respeta al otro agente)
+set -u
+
+HUB="$(cd "$(dirname "$0")/../.." && pwd)"
+cd "$HUB"
+WORKER="${1:?uso: harvest-harkaq.sh root@<ip>}"
+SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
+REMOTE="${REMOTE:-/opt/hammer}"
+SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes"
+REVIEW="tandas/needs-review-harkaq"
+mkdir -p "$REVIEW" work
+
+echo "═══ $(date -u +%FT%TZ) cosecha harkaq desde $WORKER"
+git pull --rebase -q 2>/dev/null || echo "   (pull falló; sigo con lo local)"
+
+VERD=work/harkaq-verdicts
+rm -rf "$VERD"; mkdir -p "$VERD"
+rsync -az -e "$SSH" "$WORKER:$REMOTE/work/harkaq-campana/" "$VERD/" 2>/dev/null || {
+    echo "   sin veredictos que cosechar (¿el worker sigue midiendo?)"; exit 0; }
+
+tocadas=""
+for v in "$VERD"/*.verdicts; do
+    [ -f "$v" ] || continue
+    n=$(basename "$v" .verdicts)
+    [ -f "recipes/$n.toml" ] || continue
+
+    deps=""; irr=0
+    while IFS= read -r j; do
+        [ -n "$j" ] || continue
+        echo "$j" > /tmp/.hkv.json
+        s=$(scripts/harkaq/harkaq-suggest.py /tmp/.hkv.json "$HOME/.cache/harkaq/base.policy" \
+              "$HUB/store" "$HUB/recipes" 2>/dev/null) || true
+        echo "$s" | grep -q IRREDUCIBLE && irr=1
+        # "→ declarar dep: X" (el store lo provee y sabe cuál artefacto es).
+        d=$(echo "$s" | sed -n 's/.*→ declarar dep: \([a-zA-Z0-9._-]*\).*/\1/p')
+        deps="$deps $d"
+    done < "$v"
+
+    deps=$(echo "$deps" | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ')
+    [ -n "$deps" ] || continue
+
+    if [ "$irr" = 1 ]; then
+        # NO se toca: declarar algo que el store no provee rompe el build en vez de arreglarlo.
+        cp "$v" "$REVIEW/$n.verdicts"
+        echo "   ?? $n — deuda irreducible ⇒ a needs-review (deps declarables vistas:$deps)"
+        continue
+    fi
+
+    if python3 scripts/farm/harkaq-add-deps.py "recipes/$n.toml" $deps; then
+        echo "   ++ $n — declaradas:$deps"
+        tocadas="$tocadas recipes/$n.toml"
+    fi
+done
+
+if [ -n "$tocadas" ]; then
+    # `git add` EXPLÍCITO por rutas: el repo lo comparte otro agente (cola gráfica/Go).
+    # shellcheck disable=SC2086
+    git add $tocadas
+    git commit -q -m "harkaq: declarar deps medidas por el kernel (cosecha automática)
+
+Cosecha desatendida de la campaña harkaq. Cada dep de acá la denunció el audit de
+Landlock en un build real: el kernel vio al build usarla sin declararla, y el
+store del hub confirma qué artefacto la provee (deuda DECLARABLE, §4.5).
+
+Recetas:$(echo "$tocadas" | tr ' ' '\n' | grep -v '^$' | sed 's|recipes/||;s|\.toml||' | tr '\n' ' ')
+
+No se tocó ninguna receta con deuda IRREDUCIBLE: declarar algo que el store no
+provee rompería el build en vez de arreglarlo. Esas van a $REVIEW.
+
+Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" && git push -q 2>/dev/null || {
+        git pull --rebase -q && git push -q; }
+    echo "   ✓ commit+push de $(echo "$tocadas" | wc -w) receta(s)"
+else
+    echo "   nada que declarar en este ciclo"
+fi