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
916 lines
54 KiB
Markdown
916 lines
54 KiB
Markdown
# 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 (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 takana 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 (`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 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.
|