diff --git a/docs/BRIEFING-hammer.md b/docs/BRIEFING-hammer.md new file mode 100644 index 00000000..fcc9928f --- /dev/null +++ b/docs/BRIEFING-hammer.md @@ -0,0 +1,915 @@ +# hammer — informe de arquitectura completo (briefing para un agente externo) + +> **Para qué es este documento.** Dar a un modelo que NO tiene acceso al repositorio el contexto +> suficiente para razonar sobre hammer al nivel de proponer SDDs nuevos: arquitectura, capas, +> componentes, invariantes, decisiones tomadas, estado real medido, deuda abierta y las lecciones +> de método que este proyecto pagó caro. +> +> **Fecha de corte:** 2026-08-28. Todo número marcado *(medido)* salió de un comando corrido ese +> día contra el repo; los demás llevan la fecha del documento del que vienen. +> +> **Idioma y convención:** el repo entero está en español, incluidos commits, docs y comentarios. +> Los SDD son documentos de diseño numerados (`docs/NN-*.md`); los ADR son decisiones +> (`docs/adr/NNNN-*.md`). Cuando el código y el SDD discrepan, o se corrige el código o se +> actualiza el SDD con un commit que explica por qué. + +--- + +## 0. Resumen ejecutivo en diez líneas + +`hammer` es una **distribución Linux + su gestor de construcción**, escrita en Rust, cuya tesis es +separar dos mundos hoy fusionados: + +- **El sótano (el laboratorio):** compilación determinista, hermética y direccionada por contenido + (BLAKE3). `bubblewrap` + `zig cc` + musl. Cada artefacto es un árbol de ficheros sellado bajo su + hash de **entrada**. +- **El piso de arriba (el userland):** un Linux clásico, mutable, con FHS real (`/bin`, `/lib`, + `/etc`). Los binarios se **hidratan** desde el store por hardlinks. + +Entre ambos viven tres piezas propias: **overlay** de experimentación (`try`/`commit`/`discard`), +**diario de mutaciones** por `fanotify` (la configuración se deriva *a posteriori*, no se declara +antes), y el **`.swm`**, un manifiesto de mutación que viaja en vez del binario — el receptor +**reproduce y verifica**. Encima corre un **bus de agente** (`/run/agent.sock`) por el que una IA +propone, construye, prueba en overlay y presenta; el humano hace `commit`. + +El proyecto **ya cerró** el bucle AI-nativo completo, el bootstrap auto-alojado bit-reproducible +(incluido el toolchain y el kernel), cuatro escritorios construidos desde fuente, formato de +paquete/repositorio/instalador, imágenes ISO/USB/EFI e infraestructura de granja de compilación. +Lo que falta para publicar es sobre todo legal, de claves y de validación en metal. + +--- + +## 1. Filosofía y anti-objetivos (SDD 00) + +**El problema.** Las distros inmutables/declarativas (NixOS, Guix, Silverblue) dan determinismo a +costa de agencia: el SO *es* el grafo, se pierde la estructura familiar y la IA tiene que simular a +un humano tras capas de abstracción. Las tradicionales (Arch, Gentoo) dan control pero se pudren: +el gestor pierde el rastro de lo que el usuario hizo a mano. + +**La tesis.** *«La automatización funcional es mi empleada en el sótano. En el piso de arriba mando +yo.»* Determinismo en la fábrica, libertad en la ejecución; ninguno de los dos se negocia. + +**Por qué es AI-nativo** (el diferenciador real, no la distro en sí): +1. Compilación determinista ⇒ la IA cambia fuente, recompila, y el binario es predecible. +2. El sistema ejecutable es **texto plano + binarios atómicos** ⇒ nada de bases de datos binarias + opacas (registro, journal binario). Se lee con `cat`, `grep`, `readelf`. +3. Mutabilidad = agencia real, con overlay como red de seguridad. +4. **musl** mantiene el C legible para el contexto de un LLM (glibc es un laberinto de macros). +5. El enlazado estático evita que mutar una herramienta rompa las librerías de otra. + +**Principios.** Verificar, no confiar · texto plano por encima de bases opacas · la IA propone, el +humano dispone · no reinventar lo resuelto · **el diario es la verdad** (se vive imperativamente y +el estado declarativo se deriva después, nunca al revés). + +**Anti-objetivos.** No es un gestor inmutable; no reescribe un motor de build (ADR 0004: **hammer +NO usa nix**); no compila contra `HEAD` vivo (ADR 0006); no oculta complejidad tras abstracciones. + +--- + +## 2. Arquitectura general (SDD 01) + +### 2.1 Modelo de dos mundos + +``` + EL SÓTANO (laboratorio) EL PISO DE ARRIBA (userland) + determinista · hermético mutable · clásico · FHS real + + upstreams (commits/tarballs FIJADOS) + │ + hammer-build (bwrap + zig cc) ──sella──▶ CAS store ──hidrata──▶ /bin /lib /etc + recetas + grafo de deps /store/-/ (hardlinks) + │ │ + ▼ ▼ + overlay try/commit diario fanotify + │ │ + BUS DE AGENTE /run/agent.sock (JSON-líneas) + COMPILE · INJECT · QUERY · eventos + ▼ + [ IA programadora ] +``` + +**Regla de oro:** los dos mundos nunca se mezclan. El lab jamás escribe en el FHS real (pasa por +store + hidratación); el userland jamás compila contra el host (delega al lab). + +### 2.2 Los crates (workspace Rust, edición 2021, `opt-level="z" + lto + panic=abort`) + +| Crate | Responsabilidad única | +|---|---| +| `hammer-core` | Tipos compartidos: `Recipe`, `Swm`, `Store`, `ArtifactHash`, `proto` (bus), `repo`, `sign`, `installed`, `compat` (slots), `query`, `differs`, `lab`, `apply`, `caps`, y el módulo `kernel/` (SDD 22) | +| `hammer-build` | El laboratorio: `sandbox` (bwrap), `fetch`, `download`, `hydrate`, `swm_bridge`, `harkaq` (la jaula), `config` | +| `hammer-bootstrap` | Orquesta stage0/stage1/stage2/builder/all + `manifest` (log de transparencia) | +| `hammer-overlay` | `try`/`commit`/`discard`/`status` sobre overlayfs, con apilamiento LIFO | +| `hammer-journal` | Diario append-only JSON-líneas, `content_hash`, de-dup idempotente | +| `hammer-mirror` | Replicación del store por hash entre máquinas (verifica `of_tree` al recibir) | +| `hammer-upgrade` | Generaciones, apply/rollback/prune/recover, `boot_graph` (ADR 0010) | +| `hammer-recover` | Mini-binario estático que auto-sana un upgrade interrumpido, al arranque | +| `hammer-agent` | `AgentClient`, `IntentTranslator` (+ `MockTranslator`, `ClaudeTranslator`), `Orchestrator` | +| `hammer-cli` | El binario `hammer` (+ `alpine_import`, `nix_import`, `kernel_cmd`) | +| `hammerd` | Daemon: bus de agente, watcher fanotify, FIFO de init, `crashes`, `arje_link` | +| `netup` | DHCP/netlink mínimo para el producto | + +### 2.3 Estado en disco + +``` +/store/-/ artefacto sellado, inmutable, append-only +/var/lib/hammer/ + recipes/ pins.toml journal/ overlays/ trust/ installed.json upgrades/ +/run/init.control FIFO de control humano (texto crudo) +/run/agent.sock socket de agente (JSON-líneas, SO_PEERCRED) +/run/hammer/boot-graph.json grafo de estados que dibuja el menú de arranque (ADR 0010) +``` + +--- + +## 3. Definiciones canónicas (lo que hay que entender para proponer cambios) + +### 3.1 `Recipe` — el átomo del grafo + +TOML declarativo y puro. Campos reales (`hammer-core/src/recipe.rs`): + +```toml +name = "grep" +version = "3.12" +license = "GPL-3.0-or-later" # FUERA de hash_inputs (ver §3.2) + +[source] +repo = "…" ; commit = "…" # git … XOR … +tarball = "…" ; sha256 = "…" # tarball +strip_components = 1 +patches = ["…patch"] +cargo_vendor = true # perilla del vendoreo Cargo +cargo_vendor_dir = "…" # cuando el proyecto ya trae su vendor/ + +[build] +compiler = "zig-cc" # "zig-cc" | "clang" | "gcc" +target = "x86_64-linux-musl" +link = "static" # "static" | "dynamic" +flags = [...] +zig_version = "0.13.0" # pin opcional (84 recetas lo fijan) +strip_debug = true # separa .debug_* (SDD 23) — SÍ entra al hash +cgo = false +subdir = "…" +[build.phases] configure/compile/install # overrides opcionales + +[deps] build = [...] ; runtime = [...] +[evidence] checks = [...] # H1 proof-carrying (fuera del hash) +[slots] claims = {...} ; requires = {...} # H4 compatibilidad (fuera del hash) +``` + +### 3.2 `ArtifactHash` — hash de **entrada**, no de salida + +``` +artifact_hash = BLAKE3( + source_id // "git:" XOR "tarball:" + ⧺ compiler ⧺ target ⧺ link + ⧺ lab: // ← desde 2026-08-10 + ⧺ [zig:] si está fijado + ⧺ [strip_debug:] si está fijado + ⧺ contenido de cada patch + ⧺ cada flag + ⧺ phase:configure/compile/install si hay override + ⧺ Σ dep.artifact_hash ) +``` + +Consecuencias que gobiernan casi todas las decisiones del proyecto: + +- **`hash_inputs` es una LISTA BLANCA.** Lo que no está enumerado no mueve el hash. Por eso + `license`, `evidence` y `slots` se pudieron añadir **sobre recetas ya selladas** sin re-hashear + nada. Es el truco recurrente para pagar deuda barata. +- **Las fases de build SÍ entran.** ⇒ editar el `-j` de una fase cambia el hash aunque el binario + sea idéntico; ⇒ el `.config` del kernel (que vive en la fase `configure`) **es la identidad del + artefacto** (SDD 22 §1); ⇒ `strip_debug` obliga a re-hashear el corpus (SDD 23). +- **El hash es de entrada, no de contenido.** `hammer hash ` responde en ~2 ms cuál es el + hash VIGENTE sin construir; `--check` dice si ya está sellado. `ArtifactHash::of_tree` es la otra + cosa: el hash del **contenido** real de un árbol, usado para verificar reproducibilidad y para el + mirror. +- **Corolario peligroso:** `strip` posterior al sellado rompería la verificación bit-a-bit, porque + el hash de entrada seguiría apuntando a algo que un rebuild ya no reproduce. + +### 3.3 `LabFingerprint` — el toolchain como entrada (2026-08-10) + +Se computa del **apk db del rootfs real** que va a compilar (no de un fichero declarado). Entran +compiladores/enlazadores (gcc, clang, llvm, binutils, rust, cargo), las libs de codegen de gcc +(gmp, mpfr, mpc, isl), el runtime que se enlaza dentro (musl, libgcc, libstdc++, libatomic, +libgomp) y los headers que se compilan dentro (linux-headers, fortify-headers). Quedan fuera **a +propósito** autotools y shells: es una decisión de coste, no una afirmación de que no influyen. + +**Por qué existe:** reconstruyendo los cuatro kernels en un segundo hub, el `.config` instalado salió +distinto en 4 líneas (`CONFIG_RUSTC_VERSION 109600→109700`) — Rust 1.96 vs 1.97 del rootfs, y **Rust +ni siquiera estaba activado en ese kernel**. Antes de esto, dos labs sellaban bytes distintos en la +MISMA dirección y el store no podía notarlo. + +### 3.4 Store, hidratación, overlay, diario + +- **Store**: inmutable, append-only, deduplicado, GC por alcanzabilidad (`scripts/store-gc.sh` + distingue SUPERADO —hay ejemplar mejor— de HUÉRFANO —ejemplar único—). `Store::has` rechaza + directorios vacíos (un vacío es un nombre, no un artefacto). +- **Hidratación** (ADR 0005): hardlink directo en el caso estático; en el dinámico se **copia** y + se reescribe con `patchelf --set-interpreter/--set-rpath` (no se hardlinkea, o patchelf mutaría + el inode del store). Escribir encima rompe el hardlink ⇒ el store queda intacto como base de + rollback. +- **Overlay** (SDD 04): overlayfs con `upperdir` mutable sobre el FHS real. `try`/`commit`/ + `discard`/`status`. El apilamiento es total y determinista (`created_at` + `id` + `seq`) y + `commit`/`discard` exigen **LIFO** (`Error::Shadowed` si una capa más joven sombrea el target). +- **Diario** (SDD 05): `fanotify` (FAN_CLOSE_WRITE) sobre `/bin`,`/sbin`,`/usr/bin`,`/usr/sbin`, + `/lib`,`/usr/lib`,`/etc`; JSON-líneas append-only; `op ∈ {Create, Edit, Delete}` derivado por + `classify_op`; `content_hash` tras la mutación con de-dup idempotente. Distingue mutación + **trazable** (vino de un artefacto conocido) de **opaca** (pisada a mano) sin prohibir ninguna. + +### 3.5 `.swm` — la unidad de intercambio (SDD 06) + +YAML con `base` (distro_version + pins), `mutations` y `signature` opcional (Ed25519). Cuatro tipos +de mutación: `source_patch` (una `Recipe` serializada para viajar), `config_edit` (hunks exactos, +sin fuzz), `init_rule` y `file_drop` (`content_b64` XOR `content_url`, siempre con `content_hash` +BLAKE3 verificado **antes** de escribir nada). + +`hammer apply` verifica base → verifica firma → **reproduce** en el lab local → aísla en overlay → +el humano valida → `commit`. **Nunca se ejecuta un binario ajeno.** + +### 3.6 Repositorio de paquetes (Etapa F) + +Un repo son ficheros estáticos: `index.json` + los `.swm`. Un `.swm` no tiene identidad propia; la +identidad la asigna el índice al publicar (**el índice es el namespace**). `install ` +resuelve el **cierre transitivo** de build-deps del índice (topológico), sintetiza un catálogo de +recetas efímero desde los `.swm` de las deps y reproduce desde fuente. El índice entero se firma +como **release** (cualquier `pack --repo` la invalida ⇒ re-firmar). Consumible por HTTP(S). + +--- + +## 4. El laboratorio de build (SDD 02) + +### 4.1 El sandbox + +`bubblewrap` con `--unshare-all` (net, pid, mount, ipc, uts, user). Base y deps montadas +read-only con `--overlay-src` + `--tmp-overlay /`; las únicas superficies RW son `/src` (árbol de +fuentes, bind) y `/out` (`DESTDIR`). Entorno mínimo y fijado: `SOURCE_DATE_EPOCH`, `CC`, +`-mcpu=baseline`, `CARGO_BUILD_JOBS=1`, `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1`. + +**Sin red dentro del sandbox.** El `fetch` ocurre fuera. Corolario que aparece una y otra vez: el +build es *offline por construcción*, así que su clausura **no contiene** las clases de servicio del +mundo (DNS, TLS, syslog…). + +### 4.2 Fases + +| Fase | ¿Sandbox? | Qué hace | +|---|---|---| +| `fetch` | No (red) | Clona al commit / baja el tarball y verifica sha256; vendorea Cargo/Go | +| `patch` | Sí | Aplica `source.patches` | +| `configure` | Sí | Heurística de build system: autotools / cmake / meson / cargo / make plano | +| `compile` | Sí | Compila al `target`/`link` de la receta | +| `install` | Sí | `make install DESTDIR=/out` o equivalente | +| `seal` | No | Hashea `/out` y lo deposita en el store | + +**Materialización de build-deps:** cada `deps.build` se construye recursivamente y su artefacto se +apila como capa `--overlay-src` BAJO el rootfs del sandbox, dejando sus `usr/{include,lib, +lib/pkgconfig}` en `/usr`. Así pkgconf y zig cc los encuentran sin plumbing de flags, y las recetas +sin deps quedan byte-iguales (baseline intacto). + +### 4.3 Poliglotismo + +- **C/C++**: `zig cc` por defecto (ADR 0003: un binario = compilador + musl + headers, hermético, + cross). Escotilla `compiler = "gcc"` para gcc-ismos (hoy ~16 recetas). +- **Cargo**: triple traducido (`x86_64-linux-musl` → `x86_64-unknown-linux-musl`), wrapper `zig cc` + como `CARGO_TARGET__LINKER` (cargo no acepta linker de dos palabras), `--release --locked + --offline` con `vendor/` obligatorio. `link = "dynamic"` añade `-C target-feature=-crt-static` + (necesario para `dlopen` de Vulkan/Wayland). +- **Go**: 362 recetas *(medido)*; el `go mod vendor` ocurre en el host, antes del sandbox. + +### 4.4 harkaq — la jaula (SDD 16) + +**Tesis:** bwrap da hermeticidad *respecto del host*, pero monta un **rootfs Alpine entero** como +capa base ⇒ `política ⊋ clausura`: una receta puede usar cualquier header o binario de Alpine sin +declararlo y el build pasa en verde. harkaq cierra esa brecha: **la clausura de deps declaradas ES +la política** (Landlock + seccomp), y el producto es **evidencia negativa**: `hermético ⟺ política = +clausura ∧ denegaciones = ∅`. + +Decisiones cerradas: **D1** política derivada, no escrita · **D2** evidencia negativa (por eso es +barata) · **D3** cero quieting (las denegaciones SON el producto; ABI 10 trae `ADD_RULE_QUIET` y se +decide deliberadamente no usarlo) · **D4** seccomp obligatorio, no opcional · **D7** fallo cerrado y +honesto sobre el ABI (kernel sin Landlock ⇒ build marcado *sin evidencia*, nunca fingir) · **D9** el +**canario deliberado**: `denials=[]` no se cree, se gana (se antepone un acceso que DEBE ser +denegado; si no aparece, el lector está roto). + +Estado: Fase 0 y Fase 1 cerradas (`zlib` real: `configure→Hermetico`, `compile→Impuro` por +`/usr/bin/make` no declarado); wiring en `hammer-build/src/harkaq.rs` detrás de `HARKAQ=1` e +**inerte** sin él (requisito duro: 700+ artefactos sellados no pueden cambiar de hash por encender +un diagnóstico). Barrido en granja: **0 irreducibles** ⇒ la meta de <5% de deuda se cumple. +Sutileza medida: lo que DAC ya bloquea es invisible a Landlock (el hook no llega a correr) — inocuo +para el veredicto, trampa para escribir tests. + +**Regla operativa aprendida:** el worker MIDE, el hub CLASIFICA (un store parcial infla la métrica). + +--- + +## 5. El bucle agéntico (SDD 07, 08) + +### 5.1 Dos canales + +- `/run/init.control` — FIFO de control **humano**, texto crudo (`echo "restart network" > …`). +- `/run/agent.sock` — socket de **agente**: JSON-líneas, handshake `Hello/Welcome`, auth por + `SO_PEERCRED`, capacidades leídas de `/etc/hammer/agent-caps.toml` (`default` + `[[rule]]` por + uid/gid, primera que casa gana). + +Comandos: `Compile`, `Inject`, `Query`, `Init`. Eventos: `BuildReady`, `BuildFailed` (con +`log_tail` real de 50 líneas del compilador), `Injected`, `QueryResult`, `InitAck`, `Modified`, +`Crashed`, `Error`. + +**Capacidades mínimas:** por defecto un agente sólo obtiene `query`. `compile`, `inject` (a overlay) +e **`inject-real`** (al FHS) se conceden por separado; `inject-real` exige confirmación humana por +política. + +### 5.2 El bucle + +``` +intención NL → PLAN (.swm) → BUILD (lab) → TRY (overlay) → VERIFY (tests + evidence) + → PROPOSE (diff + evidencia) → HUMANO decide commit/discard +``` + +`hammer ai --catalog F | --llm [--repair-max-attempts N]`. El traductor está detrás del +trait `IntentTranslator`: `MockTranslator` (catálogo YAML intent→`.swm`, para CI byte-a-byte) y +`ClaudeTranslator` (feature `llm-claude`, ureq+rustls). `Orchestrator::run_with_repair` drena +eventos del bus tras cada apply; si llega `Crashed`, formatea una intención nueva y reentra hasta +`max_attempts`, y el `Proposal` final lleva la `repair_chain` completa. + +**Mini-lenguaje de consulta** (`hammer query kind:value`): `bin`, `file`, `pin`, `service`, +`depends` — con parser ELF64 mínimo para extraer `DT_NEEDED`. + +--- + +## 6. Modelo de confianza (SDD 09) + +``` +fuente pública (repo@commit fijado) + patch declarado + → build determinista en TU lab local → artifact_hash + → ¿coincide con el expected_hash del autor? + sí → obtuviste su binario sin descargarlo + no → algo difiere (pin, patch, toolchain); hammer dice qué +``` + +La firma Ed25519 cubre **autoría e integridad**, no autoriza promover: `apply` **siempre** reproduce +y compara, firme o no. TrustStore local en `/var/lib/hammer/trust/*.ed25519.pub`; estados +`trusted`/`unknown-key`/`bad-sig`/`unsigned`. El **log de transparencia** es opt-in y su embrión ya +existe: `bootstrap.json` (SDD 11 §4). + +Requisitos prácticos de reproducibilidad: toolchain fijado (ya en el hash), commits fijados, sandbox +hermético, y eliminación activa de no-determinismo. **Cuando un build no reproduce es un bug a +corregir, no una excepción a tolerar.** + +--- + +## 7. Bootstrap y auto-alojamiento (SDD 11, 12; ADR 0007, 0008) — **CERRADO** + +``` +host (sólo kernel + semilla pinned) + └─ Stage 0: toolchain semilla (zig) ingerido al store, sha256 fijado ✅ + └─ Stage 1: userland mínimo cross-compilado (musl, busybox, hammerd, arje-zero) ✅ + └─ Stage 2: rebuild NATIVO dentro del rootfs Stage 1 y comparación de of_tree ✅ +``` + +Hitos verificados: + +- **Stage 1 bootea en QEMU** con **arje-zero como PID 1** (init propio, del monorepo hermano + `tawasuyu`) leyendo su seed card `/ente/seed.card.json` y supervisando hammerd + getty. El + **`CRASHED` real** que la Fase 5 había diferido está demostrado: `kill hammerd` → arje detecta + `Killed(SIGTERM)`, aplica backoff y re-encarna. +- **Stage 2 `✓ REPRODUCIBLE`** host↔VM: `of_tree(stage1') == of_tree(stage1)`. +- **Auto-alojamiento puro (variante b) CERRADO**: el toolchain del builder lo construye hammer desde + fuente. Los **6 swaps** (`make`, `busybox`, `coreutils`, `bwrap`, `linux-headers`, `rust`) + corrieron **juntos in-VM** dando `✓ REPRODUCIBLE` (`of_tree = 7fa6cb4e…`). La cadena de Rust es + purista: `mrustc → rustc 1.90.0 → 1.91.0 → 1.91.1` con `x.py` real, reusando un LLVM 20.1.8 + externo. +- **Kernel from-source CERRADO**: Linux 6.16.12 construido por hammer bootea la VM del + selfhost-verify y reproduce bit a bit. `flex`, `bison`, `openssl`, `elfutils` (sólo libelf, para + objtool) de-Alpinizados como build-deps del kernel. + +**Los dos no-determinismos que hubo que cazar** (valen como lección general): +1. `codegen-units=1` **NO bastaba**: el backend paralelo de rustc/LLVM (ThinLTO) divergía ~62 KB en + `arje-zero` a >1 CPU. Fix: `CARGO_BUILD_JOBS=1` serializa el jobserver ⇒ reproducible e + independiente del nº de CPUs. +2. El baseline "bueno" anterior (`0039b2b9…`) era un build paralelo no fiable. **Dos números + iguales de un fallo son señal, no coincidencia**; y un baseline no verificado es una foto vieja + con aspecto de dato fresco. + +--- + +## 8. Release engineering (SDD 13) — imágenes, instalador, mirror, upgrades + +- **E1 imagen auto-booteable** (GRUB BIOS i386-pc, instalado en userspace sin root con + `grub-mkimage` + `grub-bios-setup` sobre el fichero imagen). Layout GPT: BIOS-boot / `/` / + `/store` / `/var/lib/hammer`. +- **E2 instalador a disco físico**: `hammer-install ` corre dentro del live como root real + (MBR, ext2 montable como ext4, GRUB por dos `dd`). Autoinstalador: copia la propia raíz del live. +- **E3 mirror del store** (`hammer mirror push|pull|status`): CAS replicado; el receptor + **recomputa `of_tree`** y exige que case antes de sellar ⇒ una transferencia corrupta se rechaza. +- **E4 upgrades con generaciones**: apply in-place fichero-a-fichero (escritura a temporal + + `rename`), `current` como commit-point, `backup/` con los bytes previos, rollback exacto, + `prune`, y **`pending.json`** escrito ANTES de proyectar para que `recover` complete + (roll-forward) o deshaga (roll-back). `hammer-recover` (binario estático) lo hace **al arranque**, + antes de encarnar a arje-zero, y nunca aborta el boot. +- **E5 ISO/medio live**: híbrido BIOS+UEFI (dos El Torito), initramfs = product-rootfs `cpio.gz`, + todo desde RAM. **Dogfooding**: el host no traía `xorriso` ni `mtools` ⇒ los construye hammer. +- **Arranque soberano EFI-stub** sin GRUB (ADR 0010), con cero parpadeo validado por captura de + framebuffer. + +--- + +## 9. Arranque por grafo (ADR 0010) — la frontera con `tawasuyu`/mirada + +El menú de arranque **no es una lista de kernels**: es la **navegación del grafo de estados** del +sistema. hammer publica `/run/hammer/boot-graph.json` (nodos = generaciones/snapshots/base/recovery, +identificados por BLAKE3, con `parents`, `bootable`, `label`); **mirada** (compositor Wayland de +tawasuyu) lo dibuja sobre KMS y escribe la elección en `/run/hammer/boot-select`; arje-zero (PID 1) +corre `hammer boot activate --from-select` **incondicionalmente en su secuencia de apagado** — `/run` +es tmpfs, así que la selección se aplica en el apagado o no se aplica nunca. + +Invariante de UX compartido: **cero parpadeo** de DRM desde la firmware hasta el escritorio (mirada +sostiene el DRM master y nunca re-modesetea; por eso mirada real **nunca sale** y el subcomando +`hammer boot menu` es sólo harness de dev). + +--- + +## 10. El armador de kernel (SDD 22) — implementado + +Verbo `hammer kernel {stats, probe, bundles, closure, plan, diff-back, gate, hw}`. + +Los dos hechos que ordenan el diseño: + +1. **El config ES la identidad del artefacto** (vive en la fase `configure`). A favor: un "config + atestado por huella de hardware" sale casi gratis (es un `ArtifactHash` + una firma). En contra: + la explosión combinatoria es aritmética visible en el disco ⇒ **una perilla de la UI no es un + parámetro de runtime, es una edición de receta**, y `kernel plan` debe **emitir una receta + derivada**. +2. **La clausura por `depends on` NO cierra: `select` es el portillo.** Kconfig tiene al menos tres + tipos de arista (`depends on` hacia arriba, `select` hacia abajo y forzada, `imply` blanda) con + semánticas incompatibles. Hay corpus de verdad-de-campo contra el que validar: el `configure` de + `linux.toml` **ya es** un juego de bundles escrito a mano (`-d WLAN -d WIRELESS -d CFG80211…`) y + ese kernel arranca. + +Añadidos propios del SDD: **bisección sin recompilar** (kernel superset con lo bisecable como `=m` +⇒ 6 reinicios en vez de 6 rebuilds × 45 min) y **gate de no-regresión por objetivo**, no global (el +kernel de QEMU apaga USB/HID a propósito; un gate global rechazaría una receta sana). + +Probado de punta a punta el 2026-08-11: kernel a medida de `gioser`, bzImage **11,1 MB vs 14,5 +(−23%)**. Y el lab está literalmente DENTRO del artefacto: Kconfig sondea el `rustc` del entorno y +graba `CONFIG_GCC_VERSION`/`CONFIG_RUSTC_VERSION` en el `.config`. + +--- + +## 11. Los escritorios (cuatro imágenes, todas desde fuente) + +| Frente | Estado | Notas clave | +|---|---|---| +| **mirada** | perfil 33/33 *(medido)* | Compositor Wayland propio (tawasuyu) + greeter, sobre mesa software o iris | +| **KDE Plasma 6** | perfil `escritorio-kde` **163/163** *(medido)* | Hidratado rebuild-free vía `hammer hydrate --into` (install-reproduce no escala). ADR 0011 | +| **GNOME** | perfil 124/127, 3 aparcadas por diseño *(medido)* | Keystone: el shell es una **isla dinámica** — gjs `dlopen`ea `.so` reales ⇒ la introspección los exige. `mutter→gnome-shell` es HUB-ONLY estructural | +| **COSMIC** | perfil 89/90 *(medido)* | 4º escritorio usable, 6 apps + panel/dock, portal cerrado (captura da PNG real). Barato: no hay torre de C debajo | +| **wlr/sway** | perfil `escritorio-sway` **129/129** *(medido)*, arranca en QEMU | 10 recetas en una noche: wlroots, sway, yambar, fuzzel, swaybg/lock/idle, grim, slurp, wl-clipboard | + +**Stack gráfico (SDD 14):** Mesa **24.0.9** iris-only, sin LLVM y sin Vulkan. El pin es duro: desde +24.1 `iris` exige `intel-clc` ⇒ libclc + clang/LLVM. Debajo: libdrm, wayland (+ el parche +`wayland-scanner-dup2-output` — bajo musl dinámico `freopen(@OUTPUT@)` trunca el header por la +copy-relocation de `stdout`), wayland-protocols, seatd, libinput/libevdev/mtdev/libudev-zero, +libxkbcommon, pixman. + +**La lección transversal de este bloque, y probablemente la más importante del proyecto:** + +> **Sellado ≠ arranca.** El perfil `escritorio-sway` cerró **121/121, falta 0** — y entre eso y una +> pantalla con algo dibujado hubo **seis muros y ocho ciclos de imagen**: `PATH` vacío, `libz.so.1` +> ausente del rootfs, un backend de libseat que nuestra receta no construye, `/run` de sólo lectura, +> los datos XKB ausentes, y falta de tipografías. **Ninguno era una dependencia de build**, así que +> ninguno podía aparecer en el grafo. Y el método: **el veredicto es contar colores del PPM, no leer +> el log** — hubo un arranque con todo verde en el log y la captura 100 % negra. + +--- + +## 12. Infraestructura operativa (lo que no está en los SDD pero gobierna el día a día) + +### 12.1 Topología + +- **Hub principal**: laptop del autor + un segundo hub `gioser.net` (server Hetzner **protegido, sin + backup**, mismo proyecto hcloud que la granja). El store vive en un **volumen** dedicado + (bind-mount de `store`, `work/sources`, `work/repos`, `CARGO_HOME`). +- **Granja efímera**: workers hcloud on-demand (`farm-up` / `farm-run` / `farm-down`, baseline €0), + arrancados desde un snapshot **golden** que trae el toolchain + el store horneados ⇒ cache-hit + instantáneo. El worker es **compute puro y sin secretos** (ni firma ni gitea). Lleva un *dead-man* + que lo **borra** (no lo apaga — apagado seguiría facturando) tras 1 h idle. +- **Latido**: un cron cada 30 min cosecha el worker, siembra recetas y regenera el grafo. Los + commits `estado: cosecha granja ` del historial son ese latido. +- **Respaldo**: Storage Box BX11 (1 TiB), interrumpible y continuable; sube primero el *cerebro* + (estado + repo) y después el store. +- **Espejo**: `origin` empuja a gitea **y** a un espejo privado de GitHub. + +### 12.2 El khipu y la yupana — la metodología de medición + +Distinción explícita y arquitectónica, no adorno: + +- **`docs/state/build-state*.json` = el KHIPU**: el registro firme y versionado (nudos = recetas, + cuerdas = deps). Fuente única de verdad de QUÉ hay y QUÉ falta. **Se regenera** + (`scripts/build-state.py`); un generado que no se regenera es una foto vieja con aspecto de dato + fresco (error real: KDE reportaba 162/162 cuando estaba en 76/162). +- **`scripts/yupana.py` = la YUPANA**: el motor que reckona sobre el khipu. Es la **puerta única** de + la metodología. Verbos: `radio ` (¿a quién rompo si toco esto?), `perfiles`, `duplicados`, + `keystones`, `preflight`, `estado`, `objetivo`, `frontera`, `triaje`, `drenar`. + +**La regla que la justifica:** *qué nodos puntúo es una decisión de vista; **quién me consume es un +hecho** y nunca depende de la vista.* Nació de un fallo real: un cambio a `libdrm` invalidó 118 +recetas KDE, y el radio medido sólo contra `recipes/*.toml` dio **8**. Las dependencias inversas se +calculan SIEMPRE sobre todas las colas. + +**Resolución sibling-first**: una dep `libdrm` desde `incoming-kde` resuelve a `incoming-kde/libdrm` +si existe, y si no cae a `corpus/libdrm` — igual que el sandbox al apilar capas. Por eso los nodos +se identifican por **par `(cola, nombre)`**, no por nombre. + +Otros órganos: `targets.py` (perfiles → lista), `drenar.py` (orden de masticado en ondas), +`triaje.py`, `seed-graph.py --frontera`, `affected.py` (CVE por grafo), `why-differs.py`, +`verificar-repro.sh`, `store-gc.sh`, `licencias*.sh`, `fuentes/mirror-*` (ADR 0013). + +### 12.3 `targets.toml` — el manifiesto de OBJETIVO + +`build-state.json` es el grafo de lo que EXISTE; `targets.toml` es lo que la distro **debe** tener. +La inversión clave: `paquetes` lista **sólo las raíces** (lo que un usuario pide por nombre); la +clausura la calcula el grafo. Siete perfiles: `base`, `cli`, `escritorio-mirada`, `escritorio-kde`, +`escritorio-gnome`, `escritorio-cosmic`, `escritorio-sway`. Un `perfil` = una imagen enviable; +`hereda` compone; `cola` dice en qué árbol viven sus raíces. + +### 12.4 ADR 0013 — mirror de fuentes (obligación GPL, y sale barato) + +**La URL no entra en `hash_inputs`; sólo el `sha256`/commit** ⇒ los mirrors son gratis. Cerrado: +tarballs por sha256 (1,7 G) + git por bundle shallow (570/571, 2,3 G). Detalles pagados: 4 pines no +son commits sino objetos **tag** (hay que pelar con `^{commit}`); `github.com` es el 64 % del +corpus. Regla dura: **nunca cambiar el `sha256` para tapar un 404**. Vigía activo: +`fuentes-vigia.json` → **1172 fuentes, 1172 vivas, 0 muertas** *(medido)*. + +--- + +## 13. Estado real medido (2026-08-28) + +### 13.1 Corpus + +| Métrica | Valor *(medido)* | +|---|---| +| Recetas `.toml` activas | **1172** (+25 aparcadas en `*/.deferred`) | +| — corpus (`recipes/`) | 787 | +| — `incoming-kde` / `-gnome` / `-cosmic` / `-gnome-onda2` / `-wlr` / `-onda1` | 205 / 90 / 47 / 22 / 15 / 6 | +| Parches `.patch` en el árbol de recetas | 123 | +| Recetas con campo `license` | **1096 / 1197 (92 %)** | +| Recetas con `compiler = "gcc"` | **36** (16 en el corpus, 20 en las colas) | +| Recetas con `strip_debug` (campaña SDD 23) | **35**, todas en el corpus | +| Recetas que pinean `zig_version = "0.13.0"` | 84 | +| Entradas en el store local | 1654 (55 G en este disco; el store real vive en el volumen) | +| Fuentes vivas (vigía) | 1172 / 1172, 0 muertas | + +### 13.2 Grafo de estado, por vista + +| Vista | nodos | sellados | deuda | +|---|---:|---:|---:| +| corpus | 787 | 711 | **75** | +| KDE (`--kde`) | 978 | 899 | 74 (+4 `wanted`) | +| GNOME | 861 | 783 | 74 (+4 `never`) | +| COSMIC | 833 | 757 | 75 | +| wlr | 801 | 726 | 74 | + +Deuda del corpus por clase: **go 37 · rust 26 · c 12 · gui 1**. + +**Perfiles** *(medido)*: `base` 52/52 · `cli` 75/75 · `escritorio-mirada` 33/33 · +`escritorio-kde` **163/163** · `escritorio-sway` **129/129** · `escritorio-cosmic` 89/90 · +`escritorio-gnome` 124/127. + +### 13.3 Lectura de la deuda actual (importante para no malinterpretar los SDD 19/20) + +Los SDD 19/20 (2026-08-07) hablan de **«12 recetas en deuda»**. Hoy el corpus marca **75**, y **no +es una regresión de calidad**: la lista de nombres en deuda va alfabéticamente desde +`llimphi-counter`/`mise` hasta `zola` — es la **cola pendiente de la campaña de reconstrucción del +corpus en serie** (`scripts/reconstruir-corpus.sh`, reanudable, alfabética), disparada por los dos +eventos que movieron el `ArtifactHash` de todo el catálogo a la vez: + +1. **El lab entró en `hash_inputs`** (2026-08-10, §3.3). +2. **El split de debug** (SDD 23, `strip_debug`), que cambia la fase `install`. + +Y el diagnóstico de «las 12» originales fue **desmontado** después: 6 estaban SUPERADAS por la +cadena dinámica de GNOME, 1 cerrada (`wlr-randr`: le faltaban `python3` —meson es un script de +Python— y `libffi`, que exige `wayland-client.pc` en su `Requires`), 1 es frente propio +(`elfutils-libdw`, ya cerrado: dwarves construye) y 3 son HUB-ONLY estructural (fuente por SSH a +gitea privado; el worker es sin secretos por diseño). El hallazgo de fondo fue de **encolado, no +técnico**: `recipes/` (el corpus) no estaba en la lista `QUEUES` del bucle del worker — nadie las +estaba intentando. + +--- + +## 14. Deuda abierta y problemas sin resolver + +### 14.1 ADR 0012 — la carrera del árbol de fuentes (**PENDIENTE, sin decidir**) + +`fetch` nombra el árbol de forma determinista y **sin componente por-constructor**: +`work/sources/-`. Dos builds de la misma dep resuelven al MISMO directorio; uno hace +`remove_dir_all` mientras el otro corre `tar -x` y **el árbol queda roto para siempre**. + +El árbol cumple **dos papeles que se contradicen bajo concurrencia**: caché direccionada por +contenido *y* workspace mutable de build (patches, `cargo vendor`, `go mod vendor` — minutos y +decenas de GB). Por eso **un lock sólo en la extracción parece correcto y NO lo es**: un segundo +proceso puede borrar un árbol que un `bwrap` está usando para compilar. + +Medido a escala: al invalidar `libdrm`, ~93 de 205 recetas KDE murieron con `/src/.zwrap/cc is not a +full path to an existing compiler tool` — el wrapper de zig que la receta deja EN EL ÁRBOL, barrido +por el fetch concurrente de otra receta. + +Mitigación vigente (no solución): **`flock work/.farm-build.lock` alrededor de todo `hammer build`**, +tomado también por `farm-worker-loop.sh` y `campana-deuda.sh`, y `JOBS=1` en el worker. Opciones +sobre la mesa, ninguna elegida: (A) lock por árbol sostenido durante todo el build —correcto pero +serializa el diamante que converge en qtbase/kcoreaddons—, (B) árbol privado por constructor, (C) +caché inmutable + copia. + +### 14.2 No-determinismo en `.debug_*` (SDD 23) + +92 paquetes con no-determinismo probable. La causa medida **no** son rutas del host sino **rutas +internas al árbol de build que varían entre corridas** (`/src/output/meson-private`). Verificado con +`hammer why-differs`: `binutils` y `wl-clipboard` reproducen; `appstream` y `bison` **divergen**, y +la divergencia vive ENTERA en secciones `.debug_*` (el código ejecutable es idéntico). + +**La buena noticia estructural:** las dos mitades del plan son el mismo trabajo — **separar el debug +hace que el artefacto principal reproduzca**, sin tocar `-ffile-prefix-map` en ~720 recetas. Y el +tamaño: ~60 % del *artefacto* es información de depuración (el «79 %» que se citaba medía el +*contenido binario*, no el artefacto) ⇒ el espejo público baja de ~126 G a ~51 G. + +**Estado de la campaña** *(medido)*: sólo **35 recetas** declaran `strip_debug`, todas en el corpus. +Es decir: el mecanismo está implementado y el plan escrito, pero el barrido está en su arranque — +y cada tanda re-hashea lo que toca, así que compite por la misma granja que la reconstrucción del +corpus (§13.3). + +### 14.3 Otros frentes abiertos + +- **36 recetas con `compiler = "gcc"`** *(medido: 16 en el corpus + 20 en las colas de escritorio; + contadas parseando TOML, porque `grep` cuenta comentarios)*. Las 12 de Rust son **un solo + problema**: falta el unwinder de libgcc (perilla `LIBS=-lunwind` ya identificada). +- **Techo MSRV del sandbox = 1.96.** La frontera son los `*-sys` con C++ — y es **exactamente** lo + que bloquea Firefox. *Antes del navegador hay que subir el techo.* +- **`*-sys` que piden libs C sin `[deps]`**: `unable to find static system library 'lzma'/'bz2'` en + recetas Cargo. Arreglo por receta (`xz`, `bzip2`, `sqlite`), **nunca engordar el rootfs**, y + **nunca en bloque**: hay ~220 recetas Cargo sin deps y re-hashearlas invalidaría las selladas. +- **Multi-init**: los inits del catálogo (openrc, etc.) **compiten** con arje-zero, no se apilan. +- **`link=static` es a veces mentira**: cuatro causas distintas (libtool se come el `-static`; el + Makefile lo pisa; `link` es INERTE en Go; la receta Cargo pisa el lab y pierde `crt-static`). + Auditado: 0/680 mienten hoy, pero el audit **sobre-reporta si no se re-sella antes**. +- **Licencias**: 1096/1197 declaran; faltan ~101 y hay que desambiguar SPDX obsoletos (`GPL-3.0` no + dice si `-only` u `-or-later`). Regla dura: **no se rellena a ojo** — declarar mal es peor que + dejar vacío, porque convierte un hueco visible en una afirmación falsa. El cierre definitivo es + capturar la licencia en la **fase de fetch**, donde el dato pasa gratis. +- **Falta para publicar** (SDD 19/20/21): espejo de fuentes servido, **clave raíz fuera de línea** + (lo único no delegable a un agente), imagen validada en **metal** con pantalla, upgrade N→N+1 con + rollback probado, `cups` y `bluez`, documentación mínima y canal de reportes. +- **Aplicaciones gráficas de terceros: cero.** Barato: imv, zathura, mpv, gestor de archivos. + Caro: LibreOffice. El más caro del catálogo: un navegador. + +### 14.4 Modos de fallo estructurales que este proyecto descubrió (y que valen como principio) + +1. **Un artefacto VACÍO es un cache-hit.** `hammer build` miraba presencia del directorio, no + contenido ⇒ sellaba sin construir y salía OK. Siete vacíos se propagaron de respaldo → + manifiesto → grafo → store → build: **en cada eslabón el vacío se lee como presencia**. Regla: + *un ausente falla ruidosamente; un vacío llega hasta el final diciendo que todo fue bien.* +2. **El cache-hit congela regresiones.** «No re-hashea nada sellado» ≠ «es seguro»: una regla del + wrapper llevaba meses rompiendo `make` sin que se viera, porque el artefacto viejo se servía por + caché. Al reconstruir: exit 139 en medio corpus. ⇒ **tras tocar el harness, reconstruir a + propósito una receta de cada tipo.** +3. **Con flota efímera y sync unidireccional, un escritorio puede dejar de ser + reconstruible-desde-el-store sin que ningún indicador lo diga.** Pasó con KDE: los workers donde + vivían esos artefactos ya no existen, el sustrato cambió, las sombras se re-hashearon y nadie las + reconstruyó. El grafo seguía diciendo 162/162 en un JSON generado semanas antes. +4. **`grep` no es una medición en este repo.** Contó comentarios en el conteo de licencias + (`grep -l license` → 5 falsos positivos), contó `IO_URING` en un comentario del kernel, y contó + `.rela.debug_*` en la regla del `.a` no-PIC (`readelf -r | grep R_X86_64_32` **miente** si no se + excluye debug). El veredicto se obtiene **reconstruyendo y comparando**, no buscando cadenas. +5. **`.dmerge` retiene lo que borrás del store** (hardlinkea los artefactos) ⇒ podar el store no + libera disco mientras la caché los referencie. Medir con `st_nlink==1`, **nunca con `du`** (suma + los hardlinks otra vez). + +--- + +## 15. La frontera (SDD 15, 17, 18) — lo diseñado pero no cerrado + +### 15.1 SDD 15 — proof-carrying, cómputo como dato, código por contenido + +- **H1 — Proof-carrying recipes ✅**: el `.swm` lleva un bloque `evidence` (`{kind, cmd, expected}`, + `kind ∈ {cmd-exit, proptest, contract, kani}`); `hammer swm-verify --evidence` corre cada ítem + **dentro del sandbox reproducible**. La evidencia corre del lado del BUILD (separación + PROPONE/CONSTRUYE: `hammer-agent` no depende de `hammer-build`); `Event::BuildReady` lleva el + `EvidenceVerdict` y el `Orchestrator` **aborta antes de hidratar** si falla. **Frontera honesta + declarada**: garantiza que *lo declarado pasa*, no que *lo declarado basta*. +- **H2 — Memoización de ejecución ✅ (en `wawa-memo`, repo hermano)**: si el wasm es determinista, + `blake3(módulo) + blake3(entrada) + E → blake3(salida)` es una función pura y su caché es un + `LwwMap` monótono compartible por anti-entropy. `E` es un hash-de-entorno que pinea + wasmi/features/fuel/mem/ABI. Único vector residual acotado: payload de NaN vía `reinterpret`. +- **H3 — Código por contenido estilo Unison ✅ (subconjunto)**: `Base` content-addressed de + funciones wasm — identidad = `blake3(bytecode)`, nombres como metadata separada, **actualizar no + rompe** (v1 y v2 coexisten), composición por hash. **ADR 0009 decide NO reescribir la unidad de + compilación**; el puente barato es procedencia por-símbolo como metadata. +- **H4 — Configuraciones compartidas ✅**: el eje nuevo son los **slots**. Una modificación + **reclama** (`claim: slot→Id`) y **requiere** (`requires: slot→Id`). *Compatible* queda definido + duro: requisitos resueltos + reclamos disjuntos. Dos casos que el modelo trata distinto: **logo** + (dos configs escriben la misma superficie ⇒ **colisión = elección**) y **wayland** (dependencia + que no resuelve ⇒ **rechazo**). Subido a la receta real (`[slots]`, fuera de `hash_inputs`) y a la + `InstalledDb` (`system_state() → slot → hash`). CLI: `hammer compat`. + +**La ambición declarada al final del SDD 15**: `config = paquete = función = proceso`, todos el +mismo tipo de objeto direccionado por el mismo hash. + +### 15.2 SDD 17 — los diez cierres de frontera, priorizados + +**Tesis:** los tres invariantes ya pagados —**bit-reproducibilidad**, **direccionamiento por +contenido** y **clausura-como-política**— hacen casi gratis para hammer lo que a otros les cuesta. +La búsqueda correcta no es «qué frontera agregar» sino **«qué cae solo del invariante»**. + +1. **Consenso de reconstrucción** — N builders independientes reproducen el mismo hash ⇒ el binario + es confiable **sin que nadie firme nada**. Convierte la repro de QA en primitiva de distribución: + cualquiera puede ser mirror, nadie puede envenenar el repo. Nix/Debian no pueden ofrecerlo. +2. **`why-differs`** (diffoscope propio) — **implementado**: nombra la causa (un MTIME en un gzip, + la cabecera `ar` de un `.a`, qué sección ELF cambió, una ruta filtrada). Multiplicador de todo lo + demás. +3. **Política de runtime derivada del closure** — fase 3 de harkaq. Con **dos correcciones medidas**: + (a) la clausura de build **no puede** ser la política de runtime porque le **sobra** casi todo + (un `htop` musl-estático bajo la jaula **no tocó un solo fichero** fuera de sí mismo; derivarla + del build le habría concedido headers y compilador, **firmados**) ⇒ la política granular **se + MIDE corriendo el binario**; (b) la **lección casper**: hay una clase —*servicios del mundo*— + que no son paths y no caben en ninguna jaula por-fichero, y el build offline no la contiene ⇒ + **frontera** (clases `dns`/`tls-certs`/`random`/`locale-tz`/`syslog`, **bits de un `u32` + firmado** de 36 bytes canónicos) + **detalle** (paths, Landlock, medidos). +4. **CVE por grafo** — `hammer affected CVE-X` exacto + frontera mínima de rebuild + hydrate para + repartir el parche rebuild-free. Donde Debian/Alpine aproximan por nombre-versión, hammer + responde exacto. +5. **Sellar el arranque** — verity + medida; el store CAS hace fs-verity casi gratis. +6. **Matar el trusting-trust: DDC** — con la cadena mrustc cerrada, hammer es de los poquísimos que + puede **ejecutar** Diverse Double-Compiling. Un fin de semana de granja, resultado publicable. +7. **De-Alpinizar el rootfs del sandbox** — canal impuro **estructural** mientras el sandbox se + apoye en Alpine; harkaq lo mide. +8. **`hammer oci`** — imágenes distroless bit-reproducibles desde un closure. Vector de adopción + realista. +9. **Updates diferenciales content-defined** (chunking estilo casync sobre el store). +10. **Bucle agéntico con juez mecánico** — LA tesis AI-nativa: *el catálogo se cultiva solo porque + el juez es mecánico*. Sin juez, las recetas de un LLM son deuda; con él, son cosecha. Ya + demostrado a mano con `zlib` (Impuro→Hermetico en 3 fases, guiado sólo por harkaq). + +**Qué NO priorizar** (dicho explícitamente): solver SAT de versiones (el grafo es curado), multi-arch +(esperar demanda), config declarativa estilo NixOS (cancha ajena). + +### 15.3 SDD 18 — wawafs: el store como filesystem del sistema vivo + +**Diseño aceptado, sin implementar.** Bajar el checkout de userspace al kernel con la pila +**composefs** (CAS + erofs + overlayfs + fs-verity, todo mainline): no hay que escribir un +filesystem. Capas: proceso → VFS (rutas FHS reales, sin pueblo fantasma) → composefs mount → +manifest erofs por clausura (KB–MB, cero datos) → CAS de objetos → fs-verity opcional → ext4. + +Propiedades que caen solas: dedup de disco **y de page cache**, activar/rollback **O(metadata)**, +inmutabilidad estructural, y **bit-repro en runtime** (el kernel verifica el Merkle en cada lectura; +un bit podrido = EIO, no un binario silenciosamente distinto). + +**Identidad: dos granos, un mapa.** blake3 sigue siendo la identidad (recetas, sellado, grafo); el +digest fs-verity (sha256) es el *enforcement*. Misma jugada que ADR 0009. + +ADR 0005 (hardlinks) **sigue vigente** para el laptop de desarrollo; wawafs es el modo **despliegue** +(ro). El corte cae donde el workload cambia de naturaleza: se escribe una vez al sellar, se lee mil +veces al vivir. Prototipo medido (2026-07-17): `hydrate` 28 ms vs `mkcomposefs` 507 ms sobre un +rootfs de 262 MB; manifiesto `.cfs` de **136 KB firmables**. Hallazgo: **con hardlinks, un write con +privilegios a través del árbol hidratado corrompe el store** (comparten inode); con composefs es +imposible por construcción. Precondición: el kernel propio trae OVERLAY_FS+REDIRECT_DIR+METACOPY +pero **falta `EROFS_FS` y `FS_VERITY`** ⇒ re-sellado de kernel. + +--- + +## 16. Los ADR, con su estado + +| # | Decisión | Estado | +|---|---|---| +| 0001 | Rust para el tooling y los daemons | aceptada | +| 0002 | Validar sobre Alpine antes de la distro propia | aceptada | +| 0003 | `zig cc` como compilador por defecto del lab | aceptada | +| 0004 | **No escribir nuestro propio Nix** | aceptada | +| 0005 | Hidratación por hardlinks | aceptada (convive con wawafs como backend futuro) | +| 0006 | Commits fijados, no `HEAD` vivo | aceptada | +| 0007 | `arje` como init propio del track posterior | propuesta (implementada de facto) | +| 0008 | Bootstrap en 3 stages, `zig` como semilla | aceptada | +| 0009 | Código direccionado por contenido (Unison): registrar la visión, no reescribir | propuesta / design-doc | +| 0010 | Arranque por grafo: el menú de boot como navegación del grafo | aceptado; lado hammer 1–5 ✅ | +| 0011 | Etapa Escritorio: campaña KDE Plasma 6 | aceptado; gates H-Qt y H-QML pasados | +| 0012 | El árbol de fuentes es caché y workspace a la vez | **PENDIENTE, sin decidir** | +| 0013 | Mirror de fuentes: la URL es transporte, el `sha256` es la identidad | ACEPTADO, implementado | + +--- + +## 17. Superficie del CLI (`hammer`) + +``` +build · hash [--check] · why-differs · attest · hydrate +try · commit · discard · status · journal +apply · swm-verify [--evidence] · swm-sign · keygen · export +pack · install · uninstall · installed · compat · repo {list,sign,verify} +import-nix · import-alpine · pin +ai [--catalog|--llm] [--repair-max-attempts] · ctl · query +bootstrap {stage0,stage1,stage2,builder,all,manifest} +mirror {push,pull,status} +upgrade {apply,rollback,recover,status,prune} +boot {graph,activate,menu} +kernel {stats,probe,bundles,closure,plan,diff-back,gate,hw} +``` + +Daemon: `hammerd [--journal DIR] [--agent-caps FILE]`. + +--- + +## 18. Reglas del repo (CLAUDE.md) — no son estilo, son seguridad + +1. **Todo `hammer build` va envuelto en `flock work/.farm-build.lock`.** Por el ADR 0012. Para una + tanda, tomar el lock **una sola vez**, no por receta. El lock está **fuera** de `hammer build` a + propósito: los scripts de granja lo toman por fuera y hammer se bloquearía contra ellos. +2. **Nunca `git add -A`.** Varios agentes trabajan el repo a la vez; un `add -A` arrastra ficheros a + medias de otro. Commits granulares, en español, directo sobre `main`, y `git push` tras cada + unidad de trabajo. +3. **Antes de dar un artefacto por presente, mirá que tenga contenido.** (Ver §14.4.1.) + +--- + +## 19. Vocabulario del proyecto (para leer commits y docs sin perderse) + +| Término | Qué es | +|---|---| +| **el lab / el sótano** | El laboratorio de build hermético | +| **sellar** | Depositar un artefacto en el store bajo su hash | +| **hidratar** | Proyectar un artefacto del store al FHS | +| **la granja** | La flota efímera de workers hcloud | +| **el hub** | La máquina con secretos (laptop o gioser); firma, gitea, clasifica | +| **el latido** | El cron */30 que cosecha, siembra y regenera el grafo | +| **cola / queue** | Un árbol de recetas: `corpus` = `recipes/`; el resto `recipes/incoming-/` | +| **sombra** | Una receta homónima en otra cola que gana por sibling-first | +| **deuda** | Nodo del grafo sin artefacto en su hash **vigente** | +| **drenar** | Construir la deuda en ondas topológicas | +| **frontera** | Lo que upstream pide y no tenemos | +| **keystone** | Nodo en deuda que gatea el máximo trabajo bloqueado | +| **radio** | Cuántos nodos rompo si toco una receta | +| **superado / huérfano** | Artefacto con ejemplar mejor (podable gratis) / ejemplar único | +| **de-Alpinizar** | Quitarle a una receta la dependencia implícita del rootfs Alpine | +| **harkaq** | La jaula (Landlock+seccomp) que verifica que lo declarado basta | +| **yupana / khipu** | El motor de cálculo sobre el estado / el registro firme del estado | +| **arje / arje-zero** | El init propio (PID 1) del monorepo hermano `tawasuyu` | +| **mirada** | El compositor Wayland de tawasuyu que dibuja el menú de arranque | +| **wawa / wawafs** | El runtime wasm de tawasuyu / el frente de store-como-FS | + +--- + +## 20. Dónde hay hueco para SDDs nuevos + +Esto es lectura del autor de este informe, no doctrina del repo. Los huecos donde un SDD tendría +tracción real, con la razón por la que hoy no existe uno: + +1. **ADR 0012 resuelto → SDD del modelo de árbol de fuentes.** Es la única decisión estructural + marcada PENDIENTE, bloquea el paralelismo del worker (`JOBS=1`) y ya rompió una campaña entera. + Cualquier propuesta debe cubrir **extracción + build**, no sólo la extracción, y decir qué pasa + con `cargo vendor` (minutos, decenas de GB) y con el diamante que converge en qtbase. +2. **SDD del split de debug terminado** (SDD 23 está a medias): qué se hace con el paquete `-debug` + en sí, cómo viaja por el repo `.swm`, y si `-ffile-prefix-map` entra o no. Decide ~96 G de disco + y el tamaño del espejo público. +3. **SDD de gestión de claves y cadena de release.** Hoy hay firma de índice; falta clave raíz + offline, rotación, procedimiento de compromiso. Es **bloqueante para publicar** y el propio SDD + 21 lo marca como lo único no delegable a un agente. +4. **SDD de política de runtime** (cierre §3 de SDD 17): el diseño de dos niveles ya está medido + —frontera como bits de un `u32` firmado, detalle medido corriendo el binario— pero no hay + documento que lo fije ni herramienta que lo emita end-to-end. +5. **SDD de consenso de reconstrucción** (cierre §1 de SDD 17): el más diferenciador y el que + convierte la reproducibilidad de QA en primitiva de distribución. Las piezas (granja, + `bootstrap.json`, `fork-proof`, `umbral`) existen; falta el protocolo. +6. **SDD de validación de imagen: de «cierra en el grafo» a «arranca».** La lección + *sellado ≠ arranca* costó seis muros y ocho ciclos y **no tiene documento**: hoy vive como + anécdota en el §II.1 del SDD 20. Un harness reproducible (contar colores del PPM, comparar + `NEEDED` del ELF contra el rootfs, verificar datos XKB/tipografías/`/run` rw) evitaría pagarlo + una vez por perfil. +7. **SDD del catálogo de aplicaciones gráficas**, con el techo MSRV como precondición explícita: hoy + el navegador está presupuestado como frente propio pero sin plan escrito, y el orden correcto + (subir el techo → `*-sys` con C++ → Firefox) sólo aparece como nota en el SDD 21. +8. **SDD de wawafs F0–F3** ya existe (SDD 18) pero está sin implementar y sin gate ejecutado; su F0 + es barato (spike en QEMU con composefs de paquete) y decide si el frente existe. +9. **SDD de la fase de fetch como capturadora de metadatos** (licencia, y por extensión SBOM): el + propio SDD 20 identifica que ahí el dato «pasa gratis», y cerraría la deuda de licencias para + siempre en vez de por barrido. + +--- + +## 21. Cómo leer el repo (mapa de ficheros) + +``` +docs/00..23-*.md los SDD (00 visión … 23 re-hasheo) +docs/adr/ las 13 decisiones +docs/runbooks/ operativos: stage1-vm-boot, self-hosting-toolchain, kde/gnome/cosmic-desktop, + armador-de-kernel, harkaq-* +docs/state/ build-state*.json (el khipu), targets.toml (el objetivo), drenaje.json, + lab-toolchain.lock, kernel-bundles.toml, keystones.json, fuentes-vigia.json +docs/evidencia/ capturas PNG que sostienen las afirmaciones de arranque +crates/ los 12 crates del workspace +recipes/ 787 recetas del corpus + colas incoming-{kde,gnome,cosmic,wlr,…} +scripts/ yupana.py (puerta única), build-state.py, drenar.py, targets.py, + why-differs.py, store-gc.sh, licencias*.sh, verificar-repro.sh, + selfhost-verify.sh, reconstruir-corpus.sh, lab-image.sh, + farm/ (30 scripts de granja), harkaq/ (19), fuentes/ (6), rust-frontier/ (8) +store/ el CAS (bind-mount al volumen dedicado) +work/ sources/, repos/, locks, logs de campaña +``` + +**Punto de entrada recomendado para un agente nuevo:** `CLAUDE.md` → `docs/00-vision.md` → +`docs/01-architecture.md` → `docs/10-roadmap.md` → `docs/state/build-state.json` (regenerándolo) → +`scripts/yupana.py radio ` antes de tocar nada compartido.