piloto trace END-TO-END: build real de zlib trazado en store desechable — el hash REPRODUCE bit a bit el sellado (b3:2623a403), poda filosa (make 1/8 ficheros, busybox 1/2), taxonomía de ruido medida (+/cache al normalizador); harkaq-trace-build.sh orquesta (tracer por fase vía /proc/pid/root)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
3 archivos · +116 −4
diff --git a/docs/plan-freebsd-aprovechables.md b/docs/plan-freebsd-aprovechables.md
index daa44bb2..59bbc1ac 100644
--- a/docs/plan-freebsd-aprovechables.md
+++ b/docs/plan-freebsd-aprovechables.md
@@ -67,9 +67,31 @@ contrato que suggest). Piloto: 3 recetas ya instrumentadas (zlib, tar, brotli),
la traza. Bonus: los paths salen EN EL NAMESPACE DEL SANDBOX (`/usr/include/zlib.h`) —
el mismo idioma de la política harkaq, sin traducción host↔jaula. Cero código nuevo:
`harkaq-trace --fs /proc/$PID/root --prefix /usr`.
-3. **Pendiente de orquestación**: alguien tiene que darle al tracer el pid del bwrap en
- vuelo (wrapper sobre `hammer build`, o hammerd). Ese es el siguiente paso de T1.1, y
- donde toca coordinar con el harness (carril de Opus) para no duplicar lanzadores.
+3. ~~Pendiente de orquestación~~ **HECHO en forma piloto**: `harkaq-trace-build.sh` envuelve
+ un `hammer build`, engancha un tracer a cada bwrap que aparece (un bwrap por fase = una
+ traza por fase, la granularidad del veredicto) y emite canónica + sello + tabla
+ declaradas-vs-usadas. La integración sin carrera (lanzar el tracer ANTES del exec, no
+ por polling) sigue siendo del harness — coordinar con Opus.
+
+**PILOTO END-TO-END (2026-07-17, worker, `hammer build` real de zlib en store desechable
+con deps hardlinkeadas — cero contaminación):**
+
+- **El build trazado SELLÓ y el hash REPRODUJO bit a bit un artefacto ya sellado del store
+ principal** (`b3:2623a403…`): la traza no perturba el build y un store fresco reproduce —
+ mini-consenso de regalo.
+- **La señal de poda (T1.4) existe y es filosa**: de la clausura concedida, el build usó
+ `make` **1 de 8** ficheros y `busybox` **1 de 2**. Por-fichero, no por-paquete — la misma
+ granularidad que la política.
+- **Taxonomía del ruido medida** (3520 paths únicos crudos): 84 de `/cache` (cache zig
+ interna = estado derivado del build, regla nueva en el normalizador), ~3.4k del stdlib de
+ zig (`/lib/compiler_rt/*.zig` — toolchain, no clausura de receta) y los productos del
+ propio build (`example.o`, tmpfiles borrados del upper). Tras las reglas, la clausura
+ real de zlib son un puñado de paths exactos. META MODE otra vez: **las reglas de
+ normalización SON el producto**.
+- Caveats anotados: ventana ciega del polling (~decenas de ms por fase), helpers de bwrap
+ duplican marcas (la dedupe lo absorbe), y el sello del piloto es sha256 PROVISIONAL (el
+ real debe ser b3, espacio de nombres del store). Gotcha operativo: `--store` ajeno a la
+ raíz del repo rompe la resolución del rootfs (`<padre-del-store>/.dev-fs`).
---
diff --git a/scripts/harkaq/harkaq-trace-build.sh b/scripts/harkaq/harkaq-trace-build.sh
new file mode 100644
index 00000000..0d9cd832
--- /dev/null
+++ b/scripts/harkaq/harkaq-trace-build.sh
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+# harkaq-trace-build.sh — orquestador del PILOTO de traza positiva (plan-freebsd T1.1+T1.3).
+#
+# Envuelve UN `hammer build` con harkaq-trace enganchado al overlay de CADA fase: un bwrap
+# por fase ⇒ una traza por fase — la misma granularidad que el veredicto de harkaq. El
+# tracer marca `/proc/<pid-bwrap>/root`: el magic-link cae en el SB del OVERLAY, que es el
+# único lugar desde donde los reads del build se ven (la marca sobre el fs del host NO los
+# ve — ceguera-overlay, medida en el piloto; ver plan-freebsd-aprovechables.md §1). Bonus:
+# los paths salen en el namespace de la jaula (/usr/include/zlib.h) — el idioma de la
+# política, sin traducción host↔jaula.
+#
+# PILOTO, no framework: el enganche por polling tiene una ventana ciega (~decenas de ms al
+# arrancar cada fase) y bwrap puede aparecer como varios pids (helpers) — trazas duplicadas
+# que el normalizador colapsa, pero el conteo de "fases" puede inflarse. La integración
+# real (lanzar el tracer ANTES del exec del build, sin carrera) va en el harness — carril
+# de Opus, se coordina allá. Este script existe para medir HOY la señal declaradas-vs-usadas.
+#
+# uso: harkaq-trace-build.sh <receta.toml> [store] [outdir]
+# env: HAMMER (def ./target/release/hammer), TRACE (def /tmp/harkaq-trace), NORM (def
+# scripts/harkaq/harkaq-trace-norm.py)
+# deja en outdir: traza-fase-N.txt (crudas), canonica.txt, sello.txt, resumen.txt
+set -euo pipefail
+RECETA="${1:?uso: harkaq-trace-build.sh <receta.toml> [store] [outdir]}"
+STORE="${2:-./store}"
+OUT="${3:-/tmp/harkaq-trace-pilot}"
+HAMMER="${HAMMER:-./target/release/hammer}"
+TRACE="${TRACE:-/tmp/harkaq-trace}"
+NORM="${NORM:-scripts/harkaq/harkaq-trace-norm.py}"
+mkdir -p "$OUT"; rm -f "$OUT"/traza-fase-*.txt "$OUT"/canonica.txt "$OUT"/resumen.txt
+
+# bwraps que YA vivían (builds ajenos): fuera del piloto. El `|| true` importa: sin bwraps
+# pgrep sale 1 y, bajo pipefail, mataría el script en el arranque (medido, no teórico).
+YA=" $(pgrep -x bwrap 2>/dev/null | tr '\n' ' ' || true) "
+
+"$HAMMER" --store "$STORE" build "$RECETA" > "$OUT/build.out" 2> "$OUT/build.log" &
+BPID=$!
+
+fase=0
+declare -A TRAZADO
+while kill -0 "$BPID" 2>/dev/null; do
+ for p in $(pgrep -x bwrap 2>/dev/null || true); do
+ case "$YA" in *" $p "*) continue ;; esac
+ [ -n "${TRAZADO[$p]:-}" ] && continue
+ TRAZADO[$p]=1; fase=$((fase + 1))
+ # El tracer vive lo que viva SU bwrap: el cmd interno es un latido sobre el pid.
+ "$TRACE" --fs "/proc/$p/root" -o "$OUT/traza-fase-$fase.txt" -- \
+ sh -c "while kill -0 $p 2>/dev/null; do sleep 0.1; done" 2>> "$OUT/tracer.log" &
+ done
+ sleep 0.05
+done
+RC=0; wait "$BPID" || RC=$?
+wait || true # los tracers mueren con su bwrap y drenan la cola
+
+cat "$OUT"/traza-fase-*.txt > "$OUT/cruda-total.txt" 2>/dev/null || : > "$OUT/cruda-total.txt"
+python3 "$NORM" "$OUT/cruda-total.txt" > "$OUT/canonica.txt"
+
+# T1.3 (sello): b3 si hay b3sum; si no, sha256 MARCADO provisional — el sello que viaja
+# junto al of_tree debe ser b3 (mismo espacio de nombres del store), esto es el piloto.
+if command -v b3sum >/dev/null 2>&1; then
+ echo "b3:$(b3sum --no-names "$OUT/canonica.txt")" > "$OUT/sello.txt"
+else
+ echo "sha256(PROVISIONAL):$(sha256sum "$OUT/canonica.txt" | cut -d' ' -f1)" > "$OUT/sello.txt"
+fi
+
+# Declaradas vs USADAS — la señal de poda (T1.4) y el uso del runtime base (Q2 positivo).
+# Cada dep aporta un árbol al overlay fundido en /: sus paths de artefacto SON los paths
+# de jaula. `usadas/total` por dep; lo tocado que ninguna dep aporta = base Alpine.
+deps=$(sed -n '/^\[deps\]/,/^\[.*\]/p' "$RECETA" | grep -oE '"[a-zA-Z0-9_-]+"' | tr -d '"' || true)
+: > "$OUT/atribuidas.txt"
+{
+ echo "== declaradas vs usadas ($RECETA) =="
+ for d in $deps; do
+ art=$(ls -d "$STORE"/*-"$d" 2>/dev/null | head -1) || true
+ if [ -z "$art" ]; then echo " ?/? $d (sin artefacto en el store)"; continue; fi
+ cd "$art"; find . -type f | sed 's|^\.||' | sort > /tmp/.dep-files.$$; cd - >/dev/null
+ n=$(comm -12 /tmp/.dep-files.$$ <(sort "$OUT/canonica.txt") | wc -l)
+ comm -12 /tmp/.dep-files.$$ <(sort "$OUT/canonica.txt") >> "$OUT/atribuidas.txt"
+ tot=$(wc -l < /tmp/.dep-files.$$)
+ printf "%6d/%-6d %s\n" "$n" "$tot" "$d"
+ rm -f /tmp/.dep-files.$$
+ done
+ base=$(comm -23 <(sort "$OUT/canonica.txt") <(sort -u "$OUT/atribuidas.txt") | wc -l)
+ echo " base (sin dep que lo aporte): $base paths — el runtime base de Q2, medido POSITIVO"
+ echo " sello: $(cat "$OUT/sello.txt")"
+ echo " build rc=$RC — $(tail -1 "$OUT/build.out" 2>/dev/null || echo '(sin hash)')"
+} | tee "$OUT/resumen.txt"
+exit "$RC"
diff --git a/scripts/harkaq/harkaq-trace-norm.py b/scripts/harkaq/harkaq-trace-norm.py
index 53249cb2..cb8d1ac1 100644
--- a/scripts/harkaq/harkaq-trace-norm.py
+++ b/scripts/harkaq/harkaq-trace-norm.py
@@ -26,7 +26,10 @@ import sys
from collections import Counter
from pathlib import Path
-DROP_DEFAULT = ["/proc", "/sys", "/dev", "/tmp", "/out", "/src", "/run"]
+# /cache: la cache content-addressed de zig DENTRO de la jaula — estado derivado del propio
+# build (84 entradas en el piloto zlib), no entrada de la clausura. La lección META MODE
+# repite: las reglas de ruido SON el producto; cada una entra medida, no adivinada.
+DROP_DEFAULT = ["/proc", "/sys", "/dev", "/tmp", "/out", "/src", "/run", "/cache"]
def main():