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 Definiciones

🧹 store-gc: el recolector que `hammer gc` no tiene — 702 artefactos, 34G

El disco llegó a 100% y el gordo no estaba en `work/`: el store guarda un artefacto
por cada SELLADO, no uno por receta. Cuando una dep cambia, el ArtifactHash de todo
lo que cuelga cambia y el sellado nuevo SE SUMA al viejo. Medido: 1996 artefactos
para 1130 recetas — 12 copias de libqalculate, 9 de qt6-qtdeclarative, 8 de gtk4.
51G de 74G eran versiones anteriores.

El conjunto VIVO es `hammer hash` sobre todas las recetas de todas las colas: la
respuesta a "¿cuál es el hash vigente HOY?", que el store solo no puede dar. Lo que
no está ahí es rancio — pero rancio son DOS cosas muy distintas, y confundirlas es
la diferencia entre podar y perder:

  SUPERADO — su nombre TIENE un artefacto vigente presente. Versión anterior de algo
             ya sellado al día. Se reconstruye desde la receta, que está en git.
  HUÉRFANO — ningún artefacto con ese nombre es el vigente. El hash de hoy NO está
             sellado y éste es el ÚNICO ejemplar. Borrarlo sí pierde.

Por eso el default borra sólo los superados; `--huerfanos` hay que quererlo aparte.
Aplicado: 702 superados = 34G, de 5,4G libres a 43G. La verificación de que el
criterio era correcto no fue el `df`: fue que `hydrate-cosmic.sh` siguió dando 83/83
recetas y 0 faltantes.

Los 255 huérfanos (17G) quedan INTACTOS y son un hallazgo aparte: casi todos KDE
(kio×8, kparts×8, kcmutils×8), o sea que las recetas KDE se movieron después del
último sellado y el escritorio hidratado vive de artefactos que ya no son vigentes.

Dos avisos que el script lleva escritos porque cuestan al descubrirlos solos: el
espacio NO siempre se libera (los rootfs de work/ son hardlinks al store — borrar el
dir no rompe nada pero tampoco libera hasta que el rootfs se vaya), y `--store` por
defecto apunta a /store, no a ./store.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

28dc473aca0741f4120ad44eeca2cdcaf18f4d73 · por sergio · (hace 2 meses) · padre e43ce9578 · ver el árbol en este commit

Delta semántico

Ninguna definición cambió: el cambio es de texto, documentación o datos.

1 archivos · +113 −0

diff --git a/scripts/store-gc.sh b/scripts/store-gc.sh
new file mode 100755
index 00000000..a9e88a92
--- /dev/null
+++ b/scripts/store-gc.sh
@@ -0,0 +1,113 @@
+#!/usr/bin/env bash
+# store-gc.sh — recolector de basura del store content-addressed. `hammer gc` no existe; esto es.
+#
+# ── EL PROBLEMA QUE RESUELVE ────────────────────────────────────────────────────────────────────
+# El store acumula un artefacto por cada SELLADO, no uno por receta. Cuando una dep cambia, el
+# ArtifactHash de todo lo que cuelga de ella cambia, y el sellado nuevo se suma al viejo en vez de
+# reemplazarlo. Medido el 2026-08-05: 1996 artefactos para 1130 recetas — 12 copias de
+# `libqalculate`, 9 de `qt6-qtdeclarative`, 8 de `gtk4`. 51G de los 74G eran versiones anteriores.
+#
+# ── EL CRITERIO, Y POR QUÉ TIENE DOS ESCALONES ──────────────────────────────────────────────────
+# El conjunto VIVO es `hammer hash` sobre TODAS las recetas de TODAS las colas: es la respuesta a
+# «¿cuál es el hash vigente de esta receta HOY?», que el store solo no puede dar. Lo que no está en
+# ese conjunto es RANCIO — pero rancio se parte en dos cosas muy distintas:
+#
+#   SUPERADO — su nombre de receta TIENE un artefacto vigente presente en el store. Es una versión
+#              anterior de algo que ya está sellado al día. Borrarlo no pierde nada: si hiciera
+#              falta, se reconstruye desde la receta, que está en git.
+#
+#   HUÉRFANO — ningún artefacto con ese nombre es el hash vigente. O sea que el hash de HOY no está
+#              sellado y éste es el ÚNICO ejemplar que existe. Borrarlo sí pierde: es la diferencia
+#              entre «reconstruible en 2 minutos» y «hay que rehacer la torre». En la medición del
+#              2026-08-05 eran 255 artefactos (17G) y casi todos KDE (`kio`×8, `kparts`×8,
+#              `kcmutils`×8) — o sea que las recetas KDE se movieron después del último sellado y el
+#              escritorio hidratado vive de artefactos que ya no son vigentes.
+#
+# Por eso el default borra SÓLO los superados. `--huerfanos` existe para cuando lo que se quiera sea
+# exactamente eso, y hay que quererlo a propósito.
+#
+# ⚠ EL ESPACIO NO SIEMPRE SE LIBERA. Los rootfs hidratados (`work/*-rootfs`) son HARDLINKS al store.
+# Borrar el directorio del store no rompe el rootfs —los datos siguen vivos por el otro enlace— pero
+# tampoco libera el bloque hasta que el rootfs también se vaya. Es una propiedad, no un fallo.
+#
+# ⚠ NO BARRE `store-rust/` NI `store-kern/`: son stores aparte (frentes selfhost y kernel) con sus
+# propias recetas. Sus `.hammer` son manifiestos de artefacto, no recetas de entrada.
+#
+# Uso:   scripts/store-gc.sh [--aplicar] [--huerfanos]
+#        (sin --aplicar es un DRY-RUN: mide y no borra)
+# Env:   STORE (def ./store)   HAMMER (def ./target/release/hammer)   JOBS (def 8)
+set -uo pipefail
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
+STORE="${STORE:-./store}"; HAMMER="${HAMMER:-./target/release/hammer}"; JOBS="${JOBS:-8}"
+APLICAR=0; HUERFANOS=0
+for a in "$@"; do
+  case "$a" in
+    --aplicar) APLICAR=1 ;;
+    --huerfanos) HUERFANOS=1 ;;
+    *) echo "opción desconocida: $a" >&2; exit 2 ;;
+  esac
+done
+
+TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
+
+# ── 1. el conjunto VIVO ─────────────────────────────────────────────────────────────────────────
+# Todas las colas, más las recetas puestas en escena en `tandas/`. Las de tandas suelen no resolver
+# sus deps (la resolución es hermano→padre y viven fuera de `recipes/`) y su hash falla; se incluyen
+# igual porque las que SÍ resuelven son vivas de pleno derecho y omitirlas las volvería rancias.
+{ ls recipes/*.toml recipes/incoming-*/*.toml 2>/dev/null
+  find tandas -name '*.toml' 2>/dev/null | xargs grep -l '^\[source\]' 2>/dev/null
+} | sort -u > "$TMP/recetas.txt"
+echo "==> recetas barridas: $(wc -l < "$TMP/recetas.txt")"
+
+xargs -P "$JOBS" -I{} sh -c "$HAMMER --store $STORE hash \"{}\" 2>/dev/null | tail -1" < "$TMP/recetas.txt" \
+  | grep '^b3:' | sed 's/^b3://' | sort -u > "$TMP/vivos.txt"
+echo "    hashes vigentes calculables: $(wc -l < "$TMP/vivos.txt")"
+
+# Cuántas NO dieron hash. No es fatal (ver arriba) pero es el número que dice cuánta confianza tiene
+# la clasificación: si sube mucho, algo se rompió en la resolución de deps y NO hay que borrar.
+FALLOS=$(( $(wc -l < "$TMP/recetas.txt") - $(
+  xargs -P "$JOBS" -I{} sh -c "$HAMMER --store $STORE hash \"{}\" >/dev/null 2>&1 && echo ok" < "$TMP/recetas.txt" | wc -l) ))
+echo "    recetas cuyo hash FALLA: $FALLOS"
+
+# ── 2. clasificar el store ──────────────────────────────────────────────────────────────────────
+ls "$STORE" | grep -E '^[0-9a-f]{64}-' > "$TMP/dirs.txt"
+python3 - "$TMP" <<'PY'
+import sys, os
+t = sys.argv[1]
+vivos = set(open(f"{t}/vivos.txt").read().split())
+dirs = [l.strip() for l in open(f"{t}/dirs.txt") if l.strip()]
+part = lambda d: (d.split('-', 1)[0], d.split('-', 1)[1])
+nombres_vivos = {n for h, n in map(part, dirs) if h in vivos}
+sup, hue, viv = [], [], []
+for d in dirs:
+    h, n = part(d)
+    (viv if h in vivos else (sup if n in nombres_vivos else hue)).append(d)
+for f, xs in (("vigentes", viv), ("superados", sup), ("huerfanos", hue)):
+    open(f"{t}/{f}.txt", "w").write("".join(x + "\n" for x in xs))
+print(f"    artefactos: {len(dirs)} · vigentes {len(viv)} · superados {len(sup)} · huérfanos {len(hue)}")
+PY
+
+espacio() {  # du sobre una lista de dirs, tolerante a lista vacía
+  [ -s "$1" ] || { echo "0"; return; }
+  sed "s|^|$STORE/|" "$1" | tr '\n' '\0' | du -sch --files0-from=- 2>/dev/null | tail -1 | cut -f1
+}
+echo "==> espacio: superados $(espacio "$TMP/superados.txt") · huérfanos $(espacio "$TMP/huerfanos.txt")"
+
+# ── 3. borrar ───────────────────────────────────────────────────────────────────────────────────
+OBJETIVO="$TMP/superados.txt"
+if [ "$HUERFANOS" = 1 ]; then
+  cat "$TMP/superados.txt" "$TMP/huerfanos.txt" > "$TMP/objetivo.txt"; OBJETIVO="$TMP/objetivo.txt"
+  echo "!!  --huerfanos: se borran TAMBIÉN los únicos ejemplares de su receta"
+fi
+
+if [ "$APLICAR" != 1 ]; then
+  echo "==> DRY-RUN: no se borró nada. Repetí con --aplicar."
+  exit 0
+fi
+
+MANIFIESTO="work/store-gc-$(date +%Y-%m-%dT%H%M%S).txt"
+mkdir -p work; cp "$OBJETIVO" "$MANIFIESTO"
+echo "==> manifiesto de lo borrado: $MANIFIESTO"
+ANTES=$(df -h /home 2>/dev/null | tail -1 | awk '{print $4}')
+sed "s|^|$STORE/|" "$OBJETIVO" | xargs rm -rf
+echo "==> $(wc -l < "$OBJETIVO") artefactos borrados · libres: $ANTES → $(df -h /home 2>/dev/null | tail -1 | awk '{print $4}')"