Files
takana/docs/BRIEFING-takana.md
T
SergioandClaude Opus 5 008dd3925e renombre: los instaladores pasan a takana-* — y el peligro no era el fichero, era el PROTOCOLO
ADR 0016 los listaba entre los CONGELADOS; el usuario pidió descongelarlos al abrir el SDD 28. La
enmienda queda escrita en el propio ADR, que si no la etiqueta deja de describir el hecho.

`hammer-install.sh` → `takana-install.sh`, `hammer-live-install.sh` → `takana-live-install.sh`,
`hammer-banner.txt` → `takana-banner.txt`, `BRIEFING-hammer.md` → `BRIEFING-takana.md`.

**Lo que hacía caro esto no es el nombre del fichero.** El instalador se inyecta en el ISO como
`/usr/bin/hammer-install` y su éxito se detecta con un `grep` de `HAMMER-INSTALL-OK` desde TRES
scripts de prueba. Renombrar un solo lado los deja casando NADA — sin fallar —, que es literalmente
el modo en que `atribuir-fallos.py` quedó mudo cuando el renombre movió el target de `tracing`.
Se renombraron las dos puntas en el mismo commit (`/usr/bin/takana-install`,
`TAKANA-INSTALL-OK/FAIL`, `TAKANA_INSTALL_*`, `work/takana-install.img`, `/run/takana-install`),
se comprobó por `grep` que no quede ningún token viejo fuera del ADR, y —lo que decide— se CORRIÓ
`install-tui-test.sh`: 4/4 casos verdes.

Las tres `TAKANA_INSTALL_*` caen al nombre viejo (`${TAKANA_INSTALL_X:-${HAMMER_INSTALL_X:-}}`):
el llamador puede ser un ISO anterior al renombre. Misma convención que `TAKANA_ROOT_PW` unas
líneas más arriba en ese mismo script.

NO se tocó `/usr/sbin/hammer-recover` ni su hook de arranque —renombrarlo rompe máquinas YA
INSTALADAS, no el repo—: sobrevive intacto dentro del script renombrado, verificado por conteo
antes y después (8 ocurrencias). Tampoco `hammerd`, `hammer-edit` (su `name` está en la ruta del
store), `/var/lib/hammer`, `HAMMER_LIVE` ni los siete literales de hash.

De paso: `scripts/.hammer-banner.txt.kate-swp` era un swap de editor commiteado por error. Fuera.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-09 22:54:13 +00:00

916 lines
54 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# takana — 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 takana 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
`takana` 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: **takana
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)
takana-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 |
|---|---|
| `takana-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) |
| `takana-build` | El laboratorio: `sandbox` (bwrap), `fetch`, `download`, `hydrate`, `swm_bridge`, `harkaq` (la jaula), `config` |
| `takana-bootstrap` | Orquesta stage0/stage1/stage2/builder/all + `manifest` (log de transparencia) |
| `takana-overlay` | `try`/`commit`/`discard`/`status` sobre overlayfs, con apilamiento LIFO |
| `takana-journal` | Diario append-only JSON-líneas, `content_hash`, de-dup idempotente |
| `takana-mirror` | Replicación del store por hash entre máquinas (verifica `of_tree` al recibir) |
| `takana-upgrade` | Generaciones, apply/rollback/prune/recover, `boot_graph` (ADR 0010) |
| `hammer-recover` | Mini-binario estático que auto-sana un upgrade interrumpido, al arranque |
| `takana-agent` | `AgentClient`, `IntentTranslator` (+ `MockTranslator`, `ClaudeTranslator`), `Orchestrator` |
| `takana-cli` | El binario `takana` (+ `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 (`takana-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.** `takana 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).
`takana 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 `takana-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
```
`takana 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** (`takana 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); takana 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 takana 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 takana 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**: `takana-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** (`takana 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 takana.
- **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. takana 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 `takana 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
`takana boot menu` es sólo harness de dev).
---
## 10. El armador de kernel (SDD 22) — implementado
Verbo `takana 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 `takana 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 `takana 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
`takana 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.** `takana 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}`); `takana swm-verify --evidence` corre cada ítem
**dentro del sandbox reproducible**. La evidencia corre del lado del BUILD (separación
PROPONE/CONSTRUYE: `takana-agent` no depende de `takana-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: `takana 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 takana 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**`takana affected CVE-X` exacto + frontera mínima de rebuild + hydrate para
repartir el parche rebuild-free. Donde Debian/Alpine aproximan por nombre-versión, takana
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, takana 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. **`takana 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 (KBMB, 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 takana 15 ✅ |
| 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 (`takana`)
```
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 `takana 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 `takana build` a
propósito: los scripts de granja lo toman por fuera y takana 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 F0F3** 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.