docs: BRIEFING-hammer — informe de arquitectura completo para un agente externo
Reúne en un solo documento lo que hoy está repartido en 24 SDDs, 13 ADRs, los runbooks y el estado generado: arquitectura de dos mundos, definiciones canónicas (Recipe/hash_inputs/LabFingerprint/.swm/slots), el lab y sus fases, harkaq, el bucle agéntico, bootstrap auto-alojado, release engineering, arranque por grafo, los cuatro escritorios, la infraestructura de granja y la metodología yupana/khipu. Incluye estado MEDIDO al 2026-08-28 (no heredado de los docs) y explica por qué la deuda del corpus marca 75 y no las 12 del SDD 20: es la cola alfabética pendiente de la reconstrucción disparada por el lab en hash_inputs y el split de debug. Cierra con los huecos donde un SDD nuevo tendría tracción. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
This commit is contained in:
@@ -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/<b3>-<name>/ (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/<blake3>-<name>/ 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:<commit>" XOR "tarball:<sha256>"
|
||||
⧺ compiler ⧺ target ⧺ link
|
||||
⧺ lab:<LabFingerprint> // ← desde 2026-08-10
|
||||
⧺ [zig:<version>] si está fijado
|
||||
⧺ [strip_debug:<bool>] 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 <receta>` 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 <nombre>`
|
||||
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_<triple>_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 <intent> --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 <dev>` 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 <hash> --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 <ts>` 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 <receta>` (¿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/<nombre>-<sha16>`. 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-<x>/` |
|
||||
| **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 <receta>` antes de tocar nada compartido.
|
||||
Reference in New Issue
Block a user