# Registro de cambios — renaser

Registro técnico detallado, fase a fase, en orden cronológico. Formato
inspirado en *Keep a Changelog*. Para la crónica en lenguaje llano, ver
`DIARIO.md`; para el estado y los planes, `ROADMAP.md`.

---

## Fase 1 — El primer microsegundo — 2026-05-21

### Infraestructura
- Espacio de trabajo Cargo, `kernel/Cargo.toml`, `.cargo/config.toml`,
  `rust-toolchain.toml`.
- Target inicial: especificación JSON propia `x86_64-renaser.json` — PIC,
  soft-float, sin SSE, enlazador `lld`.

### Añadido
- `kernel/src/main.rs`: punto de entrada con el macro `entry_point!` de
  `bootloader_api` 0.11.
- Adopción y verificación con seguridad de tipos del framebuffer GOP (ancho,
  alto, formato de píxel, *stride*).
- Lienzo intermedio estático de 8 MiB en `.bss`, alineado a página: la técnica
  de doble búfer para evitar parpadeos.
- `#[panic_handler]` que dibuja una franja roja directamente sobre el
  framebuffer mediante escrituras volátiles.
- Dependencias: `bootloader_api` 0.11, `x86_64` 0.15, `embedded-graphics` 0.8.

### Notas
- Corrección clave: `bootloader` (constructor de imagen, lado anfitrión) no es
  `bootloader_api` (la API `no_std` que consume el kernel).

---

## Fase 1.5 — Empaquetado y arranque — 2026-05-21

### Cambiado
- Migración del target del kernel: de la JSON propia `x86_64-renaser.json` al
  target nativo precompilado `x86_64-unknown-none`. Elimina `build-std` y la
  burocracia de compilador asociada.

### Añadido
- Miembro `boot/`: orquestador de anfitrión. Dependencia de artefacto
  (RFC 3028) sobre el kernel; construcción de la imagen de disco UEFI con
  `bootloader::UefiBoot`; lanzador de QEMU.

### Toolchain
- Instalación de la toolchain nightly (la máquina solo tenía la estable).

### Corregido
- Los nightly recientes exigen `-Zjson-target-spec` para usar specs JSON
  (resuelto al migrar al target nativo).
- Sintaxis del acelerador de QEMU: `-machine q35,accel=kvm:tcg`.

### Verificado
- Arranque en QEMU: superficie índigo limpia a 1280×800.
- Baliza de pánico (franja roja) confirmada inyectando un pánico de prueba.

---

## Fase 2.0 — Cimientos del manejo de fallos — 2026-05-21

### Añadido
- `kernel/src/gdt.rs`: GDT propia, TSS y un stack de emergencia (IST)
  reservado para el manejador de doble fallo.
- `kernel/src/interrupts.rs`: IDT con manejadores de excepción de CPU. El
  breakpoint (#BP) es recuperable; #UD, #DE, #GP, #PF y #DF son fatales y
  encienden la baliza.
- `#![feature(abi_x86_interrupt)]`.

### Corregido
- La GDT debe recargar los registros SS/DS/ES con un segmento de datos del
  kernel: el cargador deja `SS = 0x10`, valor que en la GDT nueva pasa a ser el
  descriptor del TSS; el primer `iretq` de una rutina de excepción provocaba un
  #GP. Diagnosticado con la traza `-d int` de QEMU.

### Verificado
- Excepción `int3` atrapada y superada; línea-latido teal dibujada como prueba.

---

## Fase 2.1 — Interrupciones de hardware — 2026-05-21

### Añadido
- `kernel/src/pic.rs`: remapeo del par 8259 (PIC) fuera del rango de las
  excepciones (vectores 0x20+); programación del temporizador PIT a 100 Hz;
  IRQ1 de teclado.
- Bucle de render en `kernel_main`, despertado por el temporizador, en
  sustitución del `hlt` estático.

### Verificado
- La línea-latido se anima (temporizador en marcha); las pulsaciones de teclado
  inyectadas cambian el cuadro-eco (teclado en marcha).

---

## Fase 3 — Memoria dinámica y reactor asíncrono — 2026-05-21

### Añadido
- `kernel/src/memory/`: `linked_list_allocator` como `#[global_allocator]`
  sobre una región estática en `.bss`.
- `#[alloc_error_handler]`: franja naranja de agotamiento de memoria.
  `#![feature(alloc_error_handler)]`.
- `kernel/src/async_system/`: reactor cooperativo — `Executor`, `Task`/`TaskId`,
  `Waker` (vía `alloc::task::Wake`) y `ScancodeStream`.
- `kernel/src/texto.rs`: rasterización de tipografía vectorial con `fontdue`;
  TTF Adwaita Mono empotrada con `include_bytes!`.
- Dependencias: `linked_list_allocator` 0.10, `spin` 0.9, `crossbeam-queue`
  0.3, `futures-util` 0.3, `fontdue` 0.9.

### Cambiado
- Heap ampliado de 16 MiB a 64 MiB: `fontdue`, al analizar una tipografía real,
  agotaba los 16 MiB iniciales. El `#[alloc_error_handler]` lo delató pintando
  la franja naranja — el guardarraíl funcionó en silicio real.

### Corregido
- El modo `no_std` de `fontdue` está condicionado a su feature `hashbrown`; sin
  ella recae en `std`. Se fija `default-features = false, features = ["hashbrown"]`.

### Verificado
- Rótulo de bienvenida rasterizado al arranque; el texto tecleado aparece en vivo.

---

## Fase 4 — El escudo de aislamiento WASM — 2026-05-21

### Añadido
- `kernel/src/wasm/`: intérprete `wasmi` 1.0.9 en modo `no_std`. `env.rs`
  define la matriz de capacidades — exactamente dos funciones de host:
  `sys_render_frame(ptr, len)`, con validación infranqueable de los límites de
  la memoria lineal, y `sys_get_scancode()`.
- `apps/hello_wasm/`: primera aplicación del userspace — módulo
  `wasm32-unknown-unknown` (cdylib), un cuadrado móvil dirigido por teclado. Se
  empotra en el kernel con `include_bytes!`.
- Dependencia: `wasmi` 1.0.

### Cambiado
- El kernel se EXCLUYE del espacio de trabajo (`exclude = ["kernel", "apps"]`):
  al añadir `wasmi`, `cargo build` intentaba compilar el kernel para el
  anfitrión y fallaba por un símbolo `_start` duplicado. El kernel pasa a
  construirse únicamente como dependencia de artefacto de `boot`.

### Corregido
- `wasmi` `no_std` necesita `default-features = false, features = ["hash-collections"]`.
- `wasmi` 1.0.9 usa `Linker::instantiate_and_start` (no existe `instantiate`).

### Verificado
- La aplicación WASM pinta su propia superficie y responde a las pulsaciones de
  teclado inyectadas, sin más vía hacia el kernel que las dos capacidades.

---

## Mantenimiento — Estructura, documentación e integración — 2026-05-21

### Cambiado
- Refactorización: `main.rs` dividido de 692 a ~155 líneas. Se extraen los
  módulos `sync.rs`, `grafico.rs`, `consola.rs` y `baliza.rs`. Sin cambios de
  comportamiento (verificado en QEMU).

### Añadido
- Documentación: `CLAUDE.md`, `README.md`, `ARCHITECTURE.md`, `ROADMAP.md`,
  `CHANGELOG.md` y `DIARIO.md`.
- Integración con git: repositorio inicializado, remoto `origin` en Gitea,
  `.gitignore`.

### Seguridad
- `renaser.txt` —un borrador de trabajo que contenía una credencial— se subió
  por error en el commit inicial. Se purgó de todo el historial con
  `git filter-branch` y se reescribió el remoto con `push --force`. La
  credencial expuesta debe rotarse.

---

## Fase 5 — Multitarea cooperativa, guardarrail de fuel y reloj — 2026-05-22

Unificación de la Fase 3 (reactor) y la Fase 4 (WASM): el userspace deja de ser
una sola app que monopoliza la CPU y pasa a ser un conjunto de aplicaciones
cooperativas, aisladas también en el TIEMPO.

### Añadido
- `kernel/src/async_system/reloj.rs`: convierte la IRQ0 (PIT, 100 Hz) en una
  primitiva asíncrona. `CONTADOR_PULSOS` (`AtomicU64`) y `EsperaFrame`, un
  `Future` que se resuelve en el siguiente pulso — la unidad de cesión
  cooperativa del userspace. Censo de wakers tras `Mutex`, drenado por la IRQ0.
- `kernel/src/wasm`: ABI de fotograma. `AplicacionWasm` —instancia PERSISTENTE
  entre fotogramas (`Store` + `TypedFunc<(), ()>` + región)— sustituye al
  `wasm::ejecutar` de fuego-y-olvido. El módulo del userspace exporta ahora
  `init()` (una vez) y `tick()` (un fotograma, y retorna).
- Escudo de combustible: `Engine` con `Config::consume_fuel(true)` y
  `CompilationMode::Eager` (el `fuel` mide solo ejecución). Presupuesto recargado
  antes de cada `tick` — `FUEL_ARRANQUE` (20 M) y `FUEL_FOTOGRAMA` (2 M).
- `kernel/src/wasm/env.rs`: capacidad `sys_render_frame` con **regiones de
  dibujo** — cada fotograma se compone desplazado por el `(offset_x, offset_y)`
  de la app; un tamaño ajeno a la región se rechaza. Canal de teclado **por
  aplicación**: la IRQ1 difunde cada scancode a TODOS los canales, de modo que
  varias apps reciben la entrada en paralelo sin robársela.
- `kernel/src/grafico.rs`: `RegionPantalla` y `Color::DESALOJO` (púrpura).
- `apps/discola/`: aplicación WASM construida para portarse mal — su `tick` es
  un bucle cerrado. Demuestra el guardarrail de fuel en vivo.

### Cambiado
- `apps/hello_wasm`: migrada al ABI `init`/`tick`; su lienzo se dimensiona a la
  región (480×560). El estado del cuadrado persiste en su memoria lineal.
- `interrupts.rs`: la IRQ0 avanza el `reloj`; la IRQ1 difunde a los canales.
- `consola.rs`: `volcar_marco` compone en una sub-región; `pintar_desalojo`
  tatúa la baliza púrpura.
- Contención de fallos: el subsistema WASM ya no usa `.expect()`. Toda falla
  —carga, instanciación, desbordamiento, agotamiento de fuel— se devuelve como
  `Result`; la tarea desaloja la app (baliza púrpura) y el kernel sigue vivo.

### Verificado
- QEMU: tres apps concurrentes. Dos instancias de `hello_wasm` (mismo bytecode,
  regiones izquierda y derecha) renderizan y responden a W/A/S/D **en paralelo**,
  con movimiento idéntico. La app díscola es desalojada en su primer fotograma
  por agotamiento de combustible: su región queda púrpura y el sistema —kernel y
  apps vecinas— no sufre un solo sobresalto.

---

## Fase 6.0 — Cuotas de memoria y ciclo de vida del userspace — 2026-05-22

El aislamiento de las aplicaciones, que la Fase 5 hizo temporal (combustible),
se completa ahora en la dimensión espacial (memoria) y se cierra la fuga de
recursos del ciclo de vida.

### Añadido
- Techo de memoria lineal por aplicación: `4 MiB`. `wasm/mod.rs` construye un
  `StoreLimits` (`StoreLimitsBuilder::memory_size` + `trap_on_grow_failure`) y lo
  liga al `Store` con `Store::limiter`. Un `memory.grow` que rebase la cuota se
  convierte en trampa; el kernel la captura y desaloja la app.
- `FallaApp::SinMemoria` y `Color::DESALOJO_MEMORIA` (amarillo pálido). El
  desalojo distingue la causa por color: púrpura (tiempo/aborto), amarillo
  (memoria). La clasificación es robusta — `Error::as_trap_code()` da un código
  público y unívoco: `TrapCode::GrowthOperationLimited`.
- `Drop` para `AplicacionWasm`: al morir una app desalojada, su canal de teclado
  se da de baja del censo de difusión de la IRQ1 — cierra la fuga señalada en la
  Fase 5. `async_system::teclado` se reorganiza en `crear_canal` /
  `registrar_canal` / `cerrar_canal`; el canal se inscribe al FINAL de la carga,
  de modo que una carga fallida no deja canales huérfanos.
- `apps/glotona/`: aplicación WASM construida para devorar memoria — su `tick`
  invoca `memory.grow` sin freno. Demuestra el guardarrail espacial en vivo.

### Verificado
- QEMU: cuatro apps concurrentes. Las dos `hello_wasm` renderizan y responden al
  teclado en paralelo; la app díscola es desalojada por combustible (franja
  púrpura) y la app glotona por cuota de memoria (franja amarilla), ambas en su
  primer fotograma, sin que el kernel ni las apps honradas se inmuten.

---

## Fase 6.1a — Sonda PCI y disco de pruebas — 2026-05-22

Primer paso de la estrategia incremental hacia el almacenamiento: derribar el
muro del descubrimiento de hardware antes de diseñar nada encima.

### Añadido
- `kernel/src/drivers/`: nuevo subsistema de drivers. `pci.rs` enumera el bus
  PCI por fuerza bruta mediante el mecanismo de configuración #1 —puertos de E/S
  `0xCF8` (dirección) y `0xCFC` (datos)—; recorre buses y dispositivos leyendo
  el Vendor/Device ID, y localiza el disco virtio-blk (vendor `0x1AF4`, device
  `0x1001`/`0x1042`), devolviendo su ubicación y sus seis BARs.
- `kernel_main`: tras fundar la consola, sondea el bus PCI y deja constancia
  visual del hallazgo.

### Cambiado
- `boot/src/main.rs`: forja un disco de pruebas `target/disk.img` (fichero
  disperso de 32 MiB; se respeta si ya existe) y lo adjunta a QEMU como
  `virtio-blk-pci`. **Corrección de plataforma:** la máquina `q35` es x86_64 y
  exige la transmisión PCI; `virtio-blk-device` (su gemelo MMIO) es de ARM.

### Verificado
- QEMU: el kernel enumera el bus y reporta en pantalla
  `virtio-blk en bus 0 dev 3 :: BAR0 E/S 0x6000` — un dispositivo virtio-blk
  transicional (device `0x1001`), con su BAR0 en espacio de E/S. El muro del
  descubrimiento PCI queda derribado; las cuatro apps del userspace siguen
  operando sin alteración.

---

## Fase 6.1b — HAL, DMA y lectura del sector 0 — 2026-05-22

Segundo paso del sustrato de almacenamiento: el diálogo real con el disco. El
kernel monta el dispositivo virtio-blk y lee su primer sector por DMA.

### Añadido
- Dependencia `virtio-drivers` 1.13 (`no_std`, `default-features = false`,
  feature `alloc`).
- `kernel/src/drivers/disco.rs`:
  - **Asignador de marcos** «bump» — reparte páginas físicas de 4 KiB para el
    DMA, tomadas de la mayor región de RAM libre que el cargador reporta. No
    libera: suficiente para una sonda (la gestión fina llegará con el grafo).
  - **`KernelHal`** — implementa el `trait Hal` de `virtio-drivers`. `dma_alloc`
    entrega marcos físicos a cero; `mmio_phys_to_virt` traduce los BARs (el
    cargador mapea ≥ 4 GiB de memoria física, que cubre todo MMIO de PCI);
    `share`/`unshare` usan un buffer rebote para que cualquier región del kernel
    pueda viajar al dispositivo.
  - **`montar_y_leer_sector0`** — enumera el bus, habilita E/S + memoria +
    bus-master, monta el `PciTransport` y el `VirtIOBlk`, y lee el sector 0 por
    **sondeo** del *used ring* (sin depender aún de interrupciones).
- `kernel/src/drivers/pci.rs`: reescrito como `CamPuertos`, la implementación de
  `ConfigurationAccess` de `virtio-drivers` sobre los puertos `0xCF8`/`0xCFC`.
- `kernel_main` captura `physical_memory_offset` y la región de RAM de
  `BootInfo`, funda el subsistema de disco y reporta el resultado de la sonda.

### Cambiado
- `boot/src/main.rs`: al forjar el disco de pruebas graba una firma
  (`renaser-6.1b`) en su sector 0 — el testigo del viaje de ida y vuelta.

### Verificado
- QEMU: el kernel reporta `virtio-blk :: bus 0 dev 3 :: 65536 sectores ::
  s0=renaser-6.1b`. La firma grabada por el anfitrión se lee de vuelta intacta:
  descubrimiento PCI, transporte, DMA y transferencia funcionan de punta a
  punta. Las cuatro apps del userspace siguen operando sin alteración.

---

## Fase 6.1c — El grafo de objetos direccionado por contenido — 2026-05-22

Tercer y último paso del sustrato de almacenamiento. El disco deja de ser un
dispositivo que se sondea y pasa a ser una MEMORIA QUE PERDURA: un grafo
dirigido acíclico de objetos direccionados por contenido — no un sistema de
archivos plano POSIX.

### Añadido
- `kernel/src/almacen.rs` — el grafo de objetos:
  - **Objeto** — una carga útil de bytes (`datos`) y una lista de aristas
    (`hijos`: hashes de otros objetos). Las aristas hacen del almacén un DAG.
  - **Direccionamiento por contenido** — la identidad de un objeto es el hash
    BLAKE3 de su forma serializada. De ello se siguen dos propiedades que un
    sistema de archivos jamás regala: INTEGRIDAD (el contenido leído se
    rehashea y se verifica contra el hash pedido) y DEDUPLICACIÓN (contenido
    idéntico produce el mismo hash; se almacena una sola vez).
  - **Disco como log** — el sector 0 es el superbloque (magia `RENASGRF`,
    versión, cursor del log y hash de la raíz); tras él se anexan los registros
    de objetos, `[longitud u32 LE][payload postcard][relleno a cero]`. Un
    índice en memoria (hash -> sector) se reconstruye al arrancar recorriendo
    el log de cabo a rabo.
  - API: `init` —monta el disco, lee o forja el superbloque, reconstruye el
    índice—, `almacenar`, `recuperar`, `raiz` y `fijar_raiz`.
- Cinco capacidades nuevas del host en `wasm/env.rs` — `sys_object_put`,
  `sys_object_datos`, `sys_object_hijo`, `sys_object_raiz` y
  `sys_object_fijar_raiz` —, con la misma validación infranqueable de límites
  de la memoria lineal que `sys_render_frame`. Distinguen dos clases de fallo:
  un puntero inválido ABORTA la app —es su culpa, se traduce en trampa—; un
  fallo del almacenamiento le devuelve un código de error negativo —no lo es—.
- `apps/cronista/` — la primera aplicación del userspace que escribe en el
  almacenamiento PERSISTENTE. En cada arranque consulta la raíz del grafo,
  graba un objeto nuevo —`datos`: el número de arranque; `hijos`: la raíz
  anterior, el eslabón del DAG— y lo corona como raíz. Pinta una celda por
  arranque registrado y un testigo de integridad que recorre la cadena entera.
- Dependencias: `serde` 1 y `postcard` 1 (serialización binaria compacta, el
  formato que viaja al disco) y `blake3` 1 (la función hash). Las tres `no_std`.

### Cambiado
- `kernel/src/drivers/disco.rs` reescrito para un sustrato PERMANENTE:
  - El asignador de marcos «bump» de la Fase 6.1b cede el paso a uno de MAPA DE
    BITS con liberación real. `dma_dealloc` y `unshare` devuelven los marcos a
    la arena: un almacén vivo, con su trasiego incesante de DMA, ya no la agota.
  - El `VirtIOBlk` deja de montarse y destruirse en cada llamada: se monta UNA
    vez y queda tras un `Mutex` global. `leer_sectores` / `escribir_sectores`
    exponen la E/S de bloques — el disco deja de ser de solo lectura. Se retira
    `montar_y_leer_sector0`, la sonda de un solo uso de la Fase 6.1b.
- `boot/src/main.rs`: el disco de pruebas pasa a ser el disco de objetos. Ya
  no se le graba una firma — se forja virgen, a cero, y el kernel lo formatea
  la primera vez que no halle el superbloque.
- `main.rs`: `informar_disco` (la sonda de la Fase 6.1b) se sustituye por
  `informar_almacen`, que funda el grafo. El userspace pasa de cuatro a cinco
  apps; las regiones de discola y glotona se reajustan para alojar a cronista.

### Notas
- **blake3 forzado a escalar.** El target del kernel corre sin SSE; un camino
  SIMD de blake3 activado por detección en tiempo de ejecución ejecutaría
  instrucciones que la CPU, sin `CR4.OSFXSR`, rechazaría con un #UD. Se fija
  blake3 con `pure` + los cuatro `no_*` (`sse2`, `sse41`, `avx2`, `avx512`):
  implementación puramente escalar, sin SIMD ni ensamblador.

### Verificado
- QEMU, TRES arranques consecutivos sobre el mismo disco persistente: la app
  cronista pinta 1, luego 2, luego 3 celdas — la cuenta de arranques sobrevive
  a los reinicios porque vive en el grafo, en el disco, no en la RAM. El
  testigo de integridad queda VERDE en los tres: el DAG entero se recorre, de
  la raíz al primer eslabón, y su profundidad cuadra con la cuenta. El
  superbloque en disco confirma la magia `RENASGRF`, versión 1, cursor 4 y la
  raíz del tercer arranque; el registro del primer arranque guarda `datos = 1`
  y cero hijos. Las otras cuatro apps siguen su curso sin alteración — las dos
  `hello_wasm` renderizando, discola desalojada en púrpura, glotona en amarillo.

---

## Fase 6.2 — E/S de disco asíncrona por interrupción — 2026-05-22

La Fase 6.1 hizo hablar al disco, pero por SONDEO: el procesador se quedaba en
espera activa vigilando el *used ring* de virtio, incapaz de atender nada más.
La 6.2 libera el planificador cooperativo — la E/S de bloques pasa a ser
REACTIVA, guiada por la interrupción física del dispositivo.

### Añadido
- `EsperaDisco` (`drivers/disco.rs`): una transferencia de bloques expresada
  como `Future` nativo. Posee sus buferes DMA —`BlkReq`, `BlkResp` y los datos,
  en el heap para una dirección estable—; su `poll` envía la petición por la
  API NO BLOQUEANTE de `virtio-drivers` (`read_blocks_nb`/`write_blocks_nb`),
  consulta el *used ring* (`peek_used`) y, si la transferencia sigue en vuelo,
  inscribe el waker y cede. `leer_bloques`/`escribir_bloques` lo construyen.
- La IRQ del disco: `montar` descubre la línea de IRQ legada del dispositivo
  (registro «Interrupt Line», offset 0x3C del espacio de configuración PCI),
  registra un manejador en la IDT y abre la línea en el PIC.
  `interrupts::irq_disco` → `disco::atender_irq` reconoce la interrupción en el
  dispositivo —leer su registro ISR baja la línea INTx— y despierta, vía un
  waker de ranura única, a la tarea que aguardaba el bloque.
- `bloquear_en` — el puente para los contextos SÍNCRONOS (el arranque, las
  capacidades WASM, que no pueden `.await`): lleva un `Future` de disco hasta
  su final durmiendo la CPU con `hlt` —la despiertan la IRQ del disco o el
  temporizador, como red de seguridad—; jamás en espera activa con el sistema
  ya en marcha.
- `pic::desenmascarar` / `pic::vector_irq`: abrir una línea concreta del par
  8259 (con su cascada, si vive en el esclavo) y mapear línea → vector de IDT.
- `pci::linea_irq`: leer el registro «Interrupt Line» de un dispositivo.
- `tarea_sonda_disco` (`main.rs`): una tarea del reactor que lee el sector 0 de
  forma asíncrona — la prueba viva de que la IRQ conduce la E/S sin detener a
  las aplicaciones.

### Cambiado
- `leer_sectores`/`escribir_sectores` se reescriben sobre la maquinaria
  asíncrona (`bloquear_en` + `EsperaDisco`). El `almacen` y las capacidades
  `sys_object_*` NO cambian una línea: heredan la E/S por interrupción de
  forma transparente — la espera de disco deja de quemar ciclos en sondeo.
- El `VirtIOBlk` pide al dispositivo que emita interrupciones al completar cada
  petición (`enable_interrupts`).

### Decisiones de ingeniería
- **PIC, no IOAPIC.** El kernel corre íntegramente sobre el par 8259 (`pic.rs`)
  y la crate `x86_64` no ofrece abstracción de APIC. Las IRQ legadas de PCI de
  la máquina `q35` se enrutan por el 8259: basta leer la línea que el firmware
  UEFI ya asignó y abrirla. Migrar al IOAPIC habría exigido levantar LAPIC +
  IOAPIC + parseo de tablas ACPI — un subsistema entero, desproporcionado para
  el objetivo. El resultado funcional es idéntico: E/S conducida por la
  interrupción física.
- **Las capacidades WASM no `.await`-ean.** El intérprete `wasmi` ejecuta las
  funciones de host de forma SÍNCRONA: no hay manera de suspender un módulo a
  mitad de una llamada de host. Por eso `sys_object_*` no se vuelven
  asíncronas; usan `bloquear_en`, que duerme la CPU con `hlt` en lugar de
  sondear. El verdadero solapamiento E/S ↔ render lo aprovechan las TAREAS del
  reactor (`tarea_sonda_disco` hoy; la carga dinámica de módulos, mañana).
- Una IRQ legada de PCI es de NIVEL: el manejador reconoce primero al
  dispositivo —lo que baja su línea— y sólo después cierra el EOI del PIC; el
  orden inverso reavivaría la interrupción en bucle.
- Todo acceso al disco toma su `Mutex` con las interrupciones desactivadas
  (`without_interrupts`): la IRQ del disco jamás encuentra el cerrojo ocupado,
  lo que hace imposible el interbloqueo por interrupción.

### Verificado
- QEMU: el disco virtio-blk se enruta a la IRQ 11. La `tarea_sonda_disco`
  reporta «sonda asíncrona OK -- 2 IRQ de disco atendidas»: la interrupción del
  disco dispara de verdad. La app cronista, sobre el disco persistente heredado
  de la Fase 6.1c, continúa la cuenta de arranques (3 → 4 → 5 celdas) con el
  testigo de integridad del DAG en verde — la nueva E/S asíncrona lee y escribe
  el grafo sin un solo fallo. Las cuatro apps WASM siguen su curso, sin un
  sobresalto ni un micro-congelamiento.

## Fase 7 — Apertura: Apps que nacen del Grafo — 2026-05-22

Apertura de la Fase 7. Este commit asienta el plan y el andamiaje; **no
cambia el comportamiento del kernel**. La Fase 7 destierra el
`include_bytes!` del userspace: las aplicaciones pasan a ser objetos del
grafo, y un Manifiesto de Génesis —también en el grafo— dicta qué arranca,
con qué cuota y en qué región.

### Añadido
- `FASE7.md` — el plan de ataque: el objetivo, el problema huevo-y-gallina
  de la génesis, las sub-fases 7a (manifiesto + carga desde el grafo, con
  semilla por el kernel), 7b (siembra de la imagen por `boot`, muerte del
  `include_bytes!`) y 7c (persistencia inter-sesión), y los guardarraíles.
- `kernel/src/manifiesto.rs` — andamiaje del Manifiesto de Génesis. Los
  tipos `Manifiesto` / `EntradaApp` y la (de)serialización `postcard` están
  completos; `cargar` y `sembrar_genesis` son esbozos hasta la Fase 7a.
  La sub-región se guarda en campos `u32` de ancho fijo (formato en disco),
  no como `RegionPantalla` (`usize`, ancho de plataforma).

### Notas
- El módulo se declara en `main.rs` pero aún no se cablea a `kernel_main`
  (`#![allow(dead_code)]` temporal, hasta que la 7a le dé un llamador). El
  kernel compila y se comporta idéntico a la Fase 6.2 — nada observable que
  verificar en QEMU en esta apertura.

## Fase 7a — Apps que nacen del Grafo: el Manifiesto — 2026-05-22

El kernel deja de empotrar el userspace. Las cinco aplicaciones ya no llegan
por `include_bytes!` en `main.rs`: nacen del grafo de objetos, gobernadas por
un Manifiesto de Génesis que también vive en el grafo.

### Añadido
- `kernel/src/manifiesto.rs` — el Manifiesto de Génesis implementado: tipos
  `Manifiesto` / `EntradaApp`, (de)serialización `postcard`, `cargar` (lee el
  manifiesto del grafo vía el ancla del superbloque) y `sembrar_genesis`
  (puebla un disco virgen con las cinco apps de génesis).
- `almacen`: el `SuperBloque` gana el ancla `manifiesto: Option<Hash>` —
  gemela de `raiz`, pero del lado del kernel. Accesores `manifiesto()` y
  `fijar_manifiesto()`.

### Cambiado
- `almacen::VERSION` 1 → 2: el superbloque cambia de forma. Un disco v1 se
  reformatea al arrancar, como cualquier disco ajeno — la cuenta de la
  cronista reinicia una vez.
- `kernel_main`: las cinco llamadas `encender_app` escritas a mano se
  sustituyen por `cargar_userspace`, que carga el manifiesto —o lo siembra si
  el disco está virgen— e itera sus `EntradaApp`.
- `encender_app` recibe una `EntradaApp` y recupera el bytecode del grafo.
  `recuperar` ya recomputa y verifica el hash: un bytecode corrupto se niega,
  se pinta la baliza de desalojo en su región y el arranque continúa.
- `wasm::AplicacionWasm::cargar` recibe el techo de memoria por-app (del
  manifiesto) en vez de la constante global `TECHO_MEMORIA`.

### Notas
- La siembra es TRANSITORIA: el bytecode aún viaja empotrado en
  `manifiesto.rs` (`include_bytes!`) como semilla. La Fase 7b lo moverá al
  constructor de imagen `boot` y el `include_bytes!` morirá del todo.

### Verificado
- QEMU (captura headless por el monitor, `screendump`): los dos caminos del
  Manifiesto confirmados.
  - **Disco virgen** — el almacén reporta «disco formateado :: 65536 sectores
    :: 0 objetos», la consola imprime «manifiesto :: genesis sembrada en disco
    virgen» y «manifiesto :: 5 apps nacidas del grafo». La pantalla es
    idéntica a la Fase 6.2: dos `hello_wasm` renderizando, discola desalojada
    en púrpura, glotona en amarillo, cronista pintando su primera celda.
  - **Disco ya sembrado** — sin línea de siembra: el almacén reporta «grafo
    montado :: 6 objetos :: raíz presente» (las 5 semillas más el objeto que
    la cronista persistió), la consola imprime sólo «manifiesto :: 5 apps
    nacidas del grafo», y la cronista pinta su segunda celda — la cuenta de
    arranques sobrevivió al apagón. El userspace nace del grafo en ambos
    casos.

## Fase 7b — La imagen sembrada por `boot`; muere el `include_bytes!` — 2026-05-22

El kernel deja de empotrar el userspace POR COMPLETO. Hasta la 7a, el bytecode
de las cinco apps de génesis aún viajaba dentro del binario del kernel
(`include_bytes!`), como semilla transitoria. La 7b lo mata: es el constructor
de imagen `boot` —en el anfitrión— quien siembra el disco con el grafo ya
poblado. El binario del kernel ya no carga ni un solo `.wasm`.

### Añadido
- **`formato`** — nueva crate: un núcleo `#![no_std]` COMPARTIDO que define el
  formato del grafo de objetos en disco. Tipos (`Objeto`, `SuperBloque`,
  `Manifiesto`, `EntradaApp`), su (de)serialización `postcard`, la función hash
  BLAKE3 (escalar pura) y el trazado de un registro del log
  (`componer_registro` / `longitud_registro`). Lo enlazan el kernel bare-metal
  Y el anfitrión `boot`: una sola verdad del formato, imposible de divergir
  entre los dos lados. 5 pruebas de ida y vuelta. Excluida del workspace, como
  el kernel.
- `boot`: `sembrar_grafo` — siembra un disco virgen con el grafo ya poblado.
  Graba el bytecode de cada app de génesis como un objeto (deduplicado:
  `app.wasm`, usado dos veces, se guarda una sola), compone el Manifiesto de
  Génesis con sus regiones y cuotas, lo graba con las aristas hacia los objetos
  de bytecode, y forja el superbloque que lo ancla. El `.wasm` se lee de
  `kernel/assets/` en tiempo de ejecución.

### Cambiado
- `kernel/manifiesto.rs`: pierde los cuatro `include_bytes!`, el descriptor
  `AppGenesis`, `genesis()` y `sembrar_genesis()`. Se reduce a `cargar()` —leer
  el manifiesto del grafo— y `region()` —traducir la región en disco a la del
  kernel—. Los tipos `Manifiesto` / `EntradaApp` migran a `formato`.
- `kernel/almacen.rs`: los tipos `Objeto` / `SuperBloque`, las constantes del
  formato, el hash y el trazado de registros migran a `formato`. El módulo
  queda como el almacén VIVO: cursor, índice y E/S contra virtio-blk.
- `kernel_main` / `cargar_userspace`: sin la rama de siembra. Un disco sin
  manifiesto anclado ya no se siembra desde el kernel —`boot` lo hace siempre
  al forjar la imagen—; el kernel se levanta sin userspace y lo reporta.
- `kernel/Cargo.toml`: deja de declarar `serde` / `postcard` / `blake3` por su
  cuenta; los hereda —mismas features, BLAKE3 escalar puro— a través de
  `formato`.
- `wasm/mod.rs`: se retira la constante `TECHO_MEMORIA`, ya sin uso (el techo
  por-app lo dicta el manifiesto; el valor de génesis vive ahora en `boot`).
- `.cargo/config.toml`: el alias `cargo kernel` pasa de `-p kernel` a
  `--manifest-path kernel/Cargo.toml`. Con `formato` compartido entre el kernel
  y `boot`, pedir `-p kernel` dentro del workspace —con el kernel a la vez como
  dependencia de artefacto— hace caer al resolvedor de features de cargo;
  apuntar al manifiesto del kernel lo compila como raíz y esquiva el grafo.

### Verificado
- QEMU (captura headless por el monitor, `screendump`), los dos caminos:
  - **Disco virgen** — `boot` imprime «disco de objetos sembrado :: 5 objetos,
    manifiesto anclado»; el kernel monta el grafo de `boot` —«grafo montado ::
    5 objetos :: raíz ausente», SIN «disco formateado» ni «génesis sembrada»—
    e instancia las cinco apps. Pantalla idéntica a la Fase 6.2.
  - **Segundo arranque** — `boot` respeta el disco existente («el grafo
    perdura»); el kernel carga «6 objetos :: raíz presente» y la cronista pinta
    su segunda celda: la persistencia sobrevive a través de un disco sembrado
    por `boot`.
- `cargo build -p boot` y `cargo kernel` compilan limpios; `cargo test` de
  `formato` — 5/5 en verde.

## Fase 7c — Persistencia inter-sesión: cada app recuerda lo suyo — 2026-05-22

La cronista dejaba huella, pero en la RAÍZ del grafo — un ancla única que sólo
una app puede usar. La 7c estrena la persistencia POR-APP: cada `EntradaApp`
del Manifiesto de Génesis tiene su propia ranura `estado`, y una app guarda y
recobra lo suyo sin pisar a nadie. El estado de cada aplicación sobrevive, por
separado, a un reinicio.

### Añadido
- **`apps/memoriosa`** — nueva app WASM interactiva. Cuenta las teclas pulsadas
  en toda su historia y persiste el recuento; al reiniciar, despierta con su
  cuenta intacta. Una celda violeta por pulsación; el testigo de la esquina es
  verde si nació limpia, ámbar si despertó con memoria de una vida anterior.
- `kernel` — dos capacidades del host nuevas:
  - `sys_estado_cargar(salida, capacidad)` — copia el estado persistido de la
    app que llama en su memoria lineal; 0 si aún no tiene estado previo.
  - `sys_estado_guardar(datos, datos_len)` — graba `datos` como objeto del
    grafo y ancla su hash en la `EntradaApp` de la app, re-grabando y
    re-anclando el manifiesto.
- `manifiesto.rs` — el manifiesto VIVO: `VIVO`, un `Mutex<Manifiesto>` que el
  kernel custodia. `instalar` (en el arranque), `estado_de` (lee la ranura de
  una app) y `fijar_estado` (la muta, re-graba el manifiesto y lo re-ancla).
- `ContextoCapacidades` gana `indice_app`: la identidad de cada app en el
  manifiesto, con la que las capacidades de estado hallan SU ranura — jamás la
  de otra.

### Cambiado
- `AplicacionWasm::cargar` y `encender_app` reciben el `indice_app` de la app.
- `cargar_userspace` instala el manifiesto VIVO ANTES de instanciar las apps:
  el `init` de una app ya consulta su estado persistido al despertar.
- `almacen::fijar_manifiesto` deja de ser `dead_code` — lo invoca `fijar_estado`.
- La génesis (`boot`) sustituye la segunda instancia de `hola` por `memoriosa`
  (región 700,120 de 360×80).

### Verificado
- QEMU (captura headless + `sendkey` por el monitor):
  - **Disco virgen** — memoriosa arranca con 0 celdas y el testigo verde.
  - **Cinco pulsaciones** (`sendkey`) — memoriosa pinta 5 celdas violetas y
    persiste el recuento; `sys_estado_guardar` re-ancla el manifiesto.
  - **Reinicio** — `boot` respeta el disco; memoriosa despierta con las 5
    celdas intactas y el testigo en ámbar: su `init` releyó el estado del
    grafo. El estado por-app sobrevivió al apagón.

## Fase 8a — El compositor teselante — 2026-05-22

Hasta la Fase 7, cada app llevaba su región escrita a mano en el manifiesto —
coordenadas fijas, una composición rígida. La Fase 8 entrega esa decisión a un
COMPOSITOR: el kernel ya no coloca las ventanas a mano, las TESELA con
`mirada-layout`.

### Añadido
- **`kernel/compositor.rs`** — el compositor teselante. Enlaza `mirada-layout`
  —el mismo núcleo `no_std` que ordena las ventanas del compositor Wayland de
  brahman, ya enlazado por `path` cruzando la frontera de workspace— y, con su
  algoritmo `MasterStack`, calcula un marco para cada app. `disponer(n, …)`
  devuelve un marco por ventana; `area_apps`, la zona teselable (la pantalla
  menos la franja superior de la consola).
- `consola`: `pintar_escenario` —inunda el área de apps y tiñe cada marco con
  el color de panel, de modo que el teselado se vea como una rejilla— y el
  color `Color::PANEL`.

### Cambiado
- `consola::volcar_marco` ya no deposita el fotograma en `(region.x, region.y)`:
  CENTRA el fotograma natural de la app dentro de su marco teselado y lo
  recorta a sus bordes.
- `ContextoCapacidades` lleva ahora `marco` (el rectángulo teselado) y
  `natural_ancho`/`natural_alto` (el tamaño del lienzo de la app);
  `sys_render_frame` valida el fotograma contra el tamaño natural.
- `AplicacionWasm` / `encender_app` / `cargar_userspace` cablean el marco
  teselado. `cargar_userspace` recibe las dimensiones de pantalla, tesela con
  el compositor y pinta el escenario antes de encender las apps.
- `RegionPantalla::pixeles` se retira — sin uso desde que `sys_render_frame`
  mide contra el tamaño natural.
- **Las apps NO cambian.** Renderizan su lienzo natural, fijo; el kernel lo
  compone centrado en el marco. El compositor reordena la pantalla sin que
  ninguna app toque una sola instrucción.

### Verificado
- QEMU (captura headless): las cinco apps de génesis aparecen teseladas en
  `MasterStack` — `hola` como ventana maestra (izquierda, 60% del ancho) y
  memoriosa, discola, glotona y cronista apiladas a la derecha. Cada lienzo,
  centrado en su panel; las apps desalojadas (discola, glotona) tiñen su marco
  entero con la baliza. El registro de arranque permanece legible en la franja
  superior de la consola.

## Fases 8b y 8c — El escritorio interactivo — 2026-05-22

El compositor de la Fase 8a teselaba, pero era inmóvil. Las Fases 8b y 8c lo
hacen VIVO: el teclado reordena el escritorio en caliente y mueve el foco. Y
para que las apps estáticas no se queden atrás en una reordenación, el kernel
asume la persistencia visual con una caché de fotogramas.

### Añadido
- **Caché de fotogramas (backbuffer).** Cada ventana del compositor guarda, en
  RAM del kernel, el último fotograma que su app envió. La caché se reserva UNA
  vez, acotada al lienzo natural de la app —jamás crece—. Al re-teselar o mover
  el foco, el kernel recompone cada ventana desde su caché: una app que sólo
  pintó en su `init` —la cronista— conserva su imagen intacta a través de
  cualquier reordenación, sin enterarse del cambio geométrico.
- **`compositor`: el escritorio interactivo.** Un registro `ESCRITORIO` —las
  ventanas, sus marcos, sus cachés, el modo de teselado—; `presentar_fotograma`
  (cachea y compone), `desalojar`, `atender_mandos`, `ciclar_layout`,
  `mover_foco`. El foco vive en un `AtomicUsize`; los mandos del teclado, en
  una cola lock-free.
- **`tarea_compositor`** — atiende los mandos del teclado en cada fotograma del
  reactor: el contexto cooperativo donde es seguro re-teselar y recomponer.
- `Color::FOCO` (índigo brillante) y `Color::SIN_FOCO` (gris mate) — el borde
  de cada ventana, que delata de un vistazo quién tiene el foco.

### Cambiado
- **Teclado (Fase 8c).** El manejador de IRQ1 deja de difundir a ciegas:
  - La tecla **Alt** es el modificador del sistema. `Alt+Espacio` cicla el
    modo de teselado; `Alt+J` / `Alt+K` mueven el foco. Estos mandos se
    consumen en la IRQ — jamás llegan a una app.
  - Una tecla ordinaria se entrega SÓLO a la app ENFOCADA. El censo de canales
    se reindexó por `indice_app` (`Vec<Option<CanalTeclado>>`): el foco —un
    índice— elige el canal exacto.
- `consola::volcar_marco` y `pintar_desalojo` trazan el borde de foco de la
  ventana. `sys_render_frame` ya no compone directo: delega en
  `compositor::presentar_fotograma`, que cachea antes de componer.
- `AplicacionWasm` / `ContextoCapacidades` pierden el `marco` —ahora vive en el
  registro del compositor, mutable—; conservan sólo el `indice_app`.

### Exclusión de interrupciones (guardarraíl)
- El registro `ESCRITORIO` lo tocan SÓLO tareas cooperativas; el manejador de
  IRQ1 jamás lo bloquea. La IRQ se comunica con el mundo cooperativo por un
  canal estrecho y a prueba de interbloqueos: dos atómicos —el foco y el estado
  de Alt— y una cola lock-free de mandos. Ningún cerrojo en disputa entre la
  interrupción y una tarea.
- Las cachés se reservan en `compositor::fundar`, una sola vez, al tamaño
  natural de cada app: ninguna asignación dinámica dentro del bucle del reactor.

### Verificado
- QEMU (captura headless + `sendkey` por el monitor):
  - **Arranque** — cinco apps teseladas en `MasterStack`; `hola` enfocada luce
    el borde índigo, las demás el gris.
  - **`Alt+Espacio`** — el escritorio cicla a `CenteredMaster`; discola,
    glotona y cronista conservan su contenido a través del re-teselado: la
    caché de fotogramas en acción.
  - **`Alt+J`** — el foco salta de `hola` a `memoriosa`; el borde índigo se
    mueve con él.
  - **Enrutamiento** — con `memoriosa` enfocada, cuatro pulsaciones llegan sólo
    a ella —cuatro celdas violetas—; las demás apps, intactas.

## Fase 8d — Manipulación de ventanas — 2026-05-22

El escritorio de la 8c se podía recorrer con la vista —el foco— pero no
reordenar. La 8d lo hace MANIPULABLE: el orden de teselado se separa de la
identidad de las ventanas, y el teclado promueve y reordena.

### Añadido
- `Escritorio` gana `orden: Vec<usize>` — una permutación que dice qué ventana
  ocupa cada celda del teselado. Separar el orden de la identidad
  (`indice_app`) permite mover una ventana sin tocar su canal de teclado ni su
  ranura de estado: cambia de pared, no de identidad.
- `aplicar_teselado` — recalcula los marcos y los reparte según el orden.
- Mandos `Promover`, `MoverAdelante`, `MoverAtras`:
  - `Alt+Enter` — promueve la ventana enfocada a la celda maestra; las demás
    se desplazan una posición.
  - `Alt+L` / `Alt+H` — mueven la ventana enfocada adelante / atrás en el
    orden, intercambiándola con su vecina.

### Cambiado
- `mover_foco` recorre ahora el ORDEN de teselado —no los índices crudos—: el
  foco salta entre ventanas visualmente contiguas.
- `ciclar_layout` y `fundar` delegan en `aplicar_teselado`.

### Verificado
- QEMU (`sendkey`): con `memoriosa` enfocada, `Alt+Enter` la promueve a la
  ventana maestra y `hola` baja a la pila; `Alt+L` la devuelve a la pila y
  `hola` recupera la maestra. El foco —el borde índigo— viaja siempre con la
  ventana, no con la celda.

## Fase 9 — Orden-Z y ventanas flotantes — 2026-05-22

Hasta la 8d el escritorio era TESELADO puro: las ventanas repartían la pantalla
sin solaparse jamás. La Fase 9 introduce un segundo modelo de composición —el
SOLAPAMIENTO—: una ventana puede abandonar el teselado y FLOTAR, con un marco
propio y libre, por encima de las demás.

### Añadido
- `Escritorio` gana `flotantes: Vec<usize>` — la pila de ventanas flotantes en
  orden-Z, de atrás hacia adelante; `flotantes.last()` es la frontal. Junto con
  `orden` forma una partición de las ventanas: cada una está teselada o flota,
  nunca en ambas ni en ninguna.
- Mando `Flotar` (`Alt+F`): alterna la ventana enfocada entre teselada y
  flotante. Al flotar, abandona el teselado —que se recalcula para las que
  quedan—, recibe un marco propio (su lienzo natural más un reborde de cromo)
  colocado en CASCADA, y sube al frente del orden-Z. Al volver, se reincorpora
  al final del orden de teselado.
- `compositor::recomponer` — recompone el escritorio entero de una pasada: arma
  la lista de capas (las teseladas al fondo; las flotantes encima, de atrás
  hacia adelante) y se la entrega a la consola.
- `consola::recomponer` y los tipos `Capa` / `Contenido`: la consola funde las
  capas EN ORDEN sobre el lienzo; el solapamiento se resuelve por el orden del
  pintado, sin recortes ni máscaras. Una sola presentación cierra la pasada.
- `consola::componer_fotograma` — el volcado de un fotograma natural centrado
  en su marco, extraído de `volcar_marco` para compartirlo entre el camino
  rápido y la recomposición.

### Cambiado
- `presentar_fotograma` y `desalojar` bifurcan: sin ventanas flotantes
  conservan el camino RÁPIDO de la Fase 8 —pintar sólo la ventana que cambia—;
  con flotantes vivas recomponen el escritorio entero respetando el orden-Z.
- `mover_foco` recorre ahora TODAS las ventanas —las teseladas y las
  flotantes—; al enfocar una flotante, la alza al frente del orden-Z: la
  flotante con el foco está siempre delante.
- `componer_escenario`, `ciclar_layout`, `promover` y `mover_ventana` delegan
  el repintado en `recomponer`; desaparecen `redibujar_todo`,
  `redibujar_ventana` y `pintar_escenario`.

### Verificado
- QEMU (`sendkey`): `Alt+F` saca la ventana maestra del teselado y la deja
  flotando sobre las demás, que se re-teselan. Un segundo `Alt+F` sobre otra
  ventana la flota en cascada, solapando a la primera. `Alt+K` devuelve el foco
  a la ventana grande y ésta sube al frente, tapando por completo a la pequeña.
  Un `Alt+F` final la reintegra al teselado.

## Fase 10 — Alta y baja de aplicaciones en vivo — 2026-05-22

Hasta la Fase 9 el censo de aplicaciones se fijaba en el arranque: las apps
nacían del manifiesto y sólo morían al fallar. La Fase 10 lo vuelve DINÁMICO —
una app puede nacer o cerrarse con el reactor ya en marcha.

### Añadido
- **El reactor admite nacimientos en vivo.** `executor`: una cola de
  NACIMIENTOS (`Vec<FuturoTarea>` tras un `Mutex`) y la función `engendrar`.
  El ejecutor la drena al inicio de cada vuelta (`recoger_nacimientos`) y
  adopta cada futuro como tarea. `Task::adoptar` acoge un futuro ya
  empaquetado. `dormir_si_inactivo` cuenta los nacimientos como trabajo.
- **Baja en vivo — `Alt+Q`.** Mando `Cerrar`: el compositor marca la ventana
  enfocada como `cerrada`, libera su caché de respaldo, la saca del teselado y
  del orden-Z, y traslada el foco a una ventana viva contigua. La app, en su
  tarea, consulta `compositor::ventana_cerrada` cada fotograma; al verla
  cerrada concluye su tarea — y `AplicacionWasm::drop` libera su memoria
  lineal, su combustible y su canal de teclado. Una baja LIMPIA, sin baliza.
- **Alta en vivo — `Alt+N`.** Mando `Lanzar`: el compositor cuenta la petición
  (`PARTOS`); la tarea del compositor la atiende con `partos_pendientes` y
  `lanzar_app`. `compositor::nacer_ventana` añade la ventana y devuelve su
  índice; el orquestador instancia el WASM con ese índice y `engendra` su
  tarea. Las apps de génesis dejan su bytecode cacheado en RAM como
  `Plantilla`; cada `Alt+N` instancia la siguiente en rotación, sin volver al
  disco —una E/S por sondeo en mitad del reactor sería un mal vecino—.
- `compositor`: campo `Ventana.cerrada`, mandos `Cerrar` / `Lanzar`, y las
  funciones `cerrar`, `nacer_ventana`, `ventana_cerrada`, `partos_pendientes`.
- `teclado`: `Alt+Q` → `Cerrar`, `Alt+N` → `Lanzar`.

### Cambiado
- `encender_app` devuelve la `Plantilla` de la app —su bytecode y geometría—
  para los lanzamientos en vivo.
- `tarea_aplicacion` consulta `ventana_cerrada` antes de cada `tick`.
- `presentar_fotograma` y `desalojar` ignoran una ventana ya cerrada: una baja
  limpia gana a un fotograma o a un desalojo que lleguen tarde.

### Verificado
- QEMU (`sendkey`): tres `Alt+N` dan a luz tres apps nuevas y el escritorio se
  re-tesela de 5 a 8 ventanas. Tres `Alt+Q` cierran la app enfocada una a una
  y el teselado reclama su espacio, de 8 de vuelta a 5. El kernel sigue estable
  a través de todas las altas y bajas.

## Fase 11 — El reloj del sistema como capacidad de host — 2026-05-22

Hasta la Fase 10 una aplicación sólo sabía CUÁNTAS veces la habían llamado —un
`tick` tras otro—, no CUÁNTO tiempo había pasado. La Fase 11 le da al userspace
un sentido del tiempo: el reloj monótono del sistema, como capacidad.

### Añadido
- **Capacidad `sys_tiempo_mono() -> u64`** — la décima función del host. Los
  milisegundos transcurridos desde el arranque. El temporizador (PIT) ya late a
  100 Hz; `reloj` expone esa cuenta como `milisegundos()` y `env` la inyecta.
  Es una lectura PURA —no toca la memoria lineal del módulo, no hay puntero que
  validar— y MONÓTONA: jamás retrocede.
- **App `pulso`** (`apps/pulso/`, `wasm32`). Un compás visual: una cabeza
  brillante que recorre una pista y vuelve, con un período de 6 s. Su escena es
  una FUNCIÓN PURA de `sys_tiempo_mono` —no guarda estado entre fotogramas—.
  De ahí su prueba: dos instancias de `pulso`, nazca una al arrancar y otra
  mucho después con un `Alt+N`, laten exactamente al unísono.
- `pulso` se suma al userspace de génesis: `GENESIS` pasa de 5 a 6 apps, con
  `pulso` como la primera —la ventana maestra del escritorio—.

### Verificado
- QEMU (`sendkey`): la barra de `pulso` avanza con el tiempo de pared entre dos
  capturas. Un `Alt+N` da a luz un segundo `pulso` ~15 s después del arranque;
  flotado junto al primero, su barra está en la MISMA fase — la prueba de que
  el compás se rige por el reloj absoluto, no por una cuenta de fotogramas.

## Fase 12 — La bocina del PC como capacidad de host — 2026-05-22

La Fase 11 le dio al userspace un reloj; la Fase 12, una voz. La bocina del PC
—el canal 2 del PIT como generador de onda cuadrada— se suma a la matriz de
capacidades. Hasta hoy renaser sólo sabía DIBUJAR para llamar la atención.

### Añadido
- **Driver `drivers/altavoz`.** Programa el canal 2 del PIT a una frecuencia y
  abre la compuerta del puerto 0x61. El canal 0 del PIT es el latido del
  kernel; el canal 2 es de la bocina y de nadie más — no se perturban—.
  `tono(hz)` es su única vía; un `0` la silencia.
- **Capacidad `sys_tono(frecuencia_hz)`** — la undécima función del host. La
  bocina es un recurso ÚNICO y global: para que dos apps no se la disputen,
  pertenece —como el teclado desde la Fase 8c— a la ventana ENFOCADA. Una app
  sin foco puede pedir un tono; sencillamente, no se oye. Al cambiar el foco,
  el compositor calla la bocina (`mover_foco`, `cerrar`): la nueva dueña la
  reclama en su próximo fotograma.
- **App `tonada`** (`apps/tonada/`, `wasm32`). Toca una escala de Do mayor en
  bucle y la dibuja como una escalera de ocho barras, con la nota que suena
  encendida. Junta las dos capacidades nuevas del kernel: el reloj
  (`sys_tiempo_mono`) le marca el compás, la bocina (`sys_tono`) le da voz.
- `tonada` encabeza el userspace de génesis: `GENESIS` pasa de 6 a 7 apps, con
  `tonada` como la ventana maestra —enfocada al arrancar, así suena de
  inmediato—.

### Verificado
- QEMU. Visual: la escalera de `tonada` enciende la nota en curso y ésta
  recorre la escala con el tiempo (capturas con la 6.ª y la 1.ª nota vivas).
  Sonido: con la bocina enrutada a un WAV (`-audiodev wav` + `-machine
  pcspk-audiodev`), el PCM capturado —50 s— es una onda cuadrada oscilante de
  ±24 000 de amplitud, con una frecuencia media de ~375 Hz: la de la escala de
  Do mayor.

## Fase 13 — Ratón, puntero y arrastre de flotantes — 2026-05-22

renaser tenía teclado y bocina como entrada/salida del usuario, pero no
puntero. Las ventanas flotantes de la Fase 9 nacían en cascada y se quedaban
ahí, clavadas. La Fase 13 trae el ratón: un puntero en pantalla, clic-para-
enfocar y arrastre del marco de las ventanas flotantes.

### Añadido
- **Driver `drivers/raton`** — el ratón PS/2 cuelga del dispositivo auxiliar
  del 8042 (la misma controladora que el teclado) y anuncia cada movimiento
  por la IRQ12. El driver despierta el aux, enciende su IRQ, le ordena
  reportar, y ensambla los paquetes de 3 bytes con la guarda del bit-3 que
  detecta desincronización. Como el teclado, la IRQ12 sólo toca atómicos
  —posición del puntero— y una cola lock-free de eventos.
- **El puntero, capa de presentación.** `Pantalla` gana `formato` y la
  función `estampar_puntero`: un sprite de flecha de 12×18 que se pinta
  DIRECTAMENTE sobre el framebuffer, justo después de copiar el lienzo. El
  lienzo permanece libre de puntero —hace de save-under natural—; cada
  `presentar` sella el puntero al final.
- **Capacidad del compositor**: `atender_raton` drena eventos del ratón cada
  fotograma. El botón izquierdo bajando es un CLIC: enfoca la ventana viva
  bajo el puntero (consistente con `mover_foco` — silencia la bocina, alza al
  frente si flota). Si la enfocada es flotante, arranca un ARRASTRE con el
  desfase de agarre; con el botón sostenido, la ventana sigue al puntero;
  soltarlo lo termina.
- **`refrescar_puntero`**: en una vuelta tranquila en que ninguna app pinte,
  reestampa el framebuffer si el puntero se ha movido — el centinela
  empacado evita repintar dos veces el mismo instante.
- `Escritorio` gana `arrastre: Option<Arrastre>` y `raton_izq: bool`.
  `Arrastre` guarda el índice de la ventana asida y el desfase de agarre.
  `cerrar` libera el arrastre si la ventana cerrada era la arrastrada.

### Verificado
- QEMU (`mouse_move` + `mouse_button` del monitor). El puntero aparece en el
  centro al arrancar; `mouse_move` lo lleva por la pantalla. Clic sobre la
  ventana `pulso` (en la pila) la enfoca: el borde índigo deja la maestra y
  envuelve a `pulso`. `Alt+F` la flota; el ratón la agarra y la arrastra de
  la esquina superior izquierda al centro-abajo de la pantalla con el botón
  sostenido. El kernel sigue estable a través de los gestos.

## Mapeador MMIO en el kernel — 2026-05-23

OVMF aloja los BAR prefetchables 64-bit de virtio en la ventana PCI de 64
bits, que el `bootloader_api` no mapea. `KernelHal::mmio_phys_to_virt`
devolvía `phys + offset` a ciegas y el primer registro MMIO leído reventaba
con #PF, dejando una franja roja sin pista visible. La autopsia: el commit
anterior añadió el dump del panic-handler por COM1, que sacó a la luz el IP
en `unchecked_mul::precondition_check` y la dirección de fallo
`offset + 0x800000014` — phys 32 GiB, donde OVMF había puesto el BAR.

### Añadido
- `memory::mmio`: envuelve la tabla L4 activa (vía `Cr3` + el mapeo de
  memoria física del cargador) en un `OffsetPageTable`. `mapear(fisica,
  tam)` abre páginas hacia la región pedida con `PRESENT | WRITABLE |
  NO_CACHE | WRITE_THROUGH`, tratando `PageAlreadyMapped` y
  `ParentEntryHugePage` como éxito silencioso.
- Los marcos para tablas intermedias salen del banco DMA del disco
  (`asignar_marco_para_tabla`, sin pánico).
- `KernelHal::mmio_phys_to_virt` llama a `memory::mmio::mapear` antes de
  devolver el puntero, asegurándose de que el BAR sea accesible.

### Cambiado
- `baliza::Serie` es ahora `pub(crate)`: cualquier módulo puede dejar
  trazas por COM1. El panic-handler y el OOM-handler dejan en COM1 su
  mensaje y ubicación además de pintar la franja. `kernel_main` traza
  cada hito del arranque para diagnóstico remoto.

## Fase 14 — La identidad del escritorio: nombres y barra de tareas — 2026-05-23

Las ventanas eran anónimas: el escritorio reía con sus colores pero no sabía
cómo se llama lo que muestra. La Fase 14 le da nombre a cada cuarto y una
barra al pie con la lista de quien vive en la casa.

### Añadido
- Cada `Ventana` lleva un `nombre: String` —el que dicta su `EntradaApp` del
  manifiesto, o el que el orquestador pasa al engendrarla en vivo (Fase 10)—.
  `Plantilla` se acuerda del nombre para los lanzamientos posteriores.
- **`FRANJA_TASKBAR`** (40 px al pie) y `area_taskbar`: la barra de tareas
  vive en su propia franja. `area_apps` la descuenta — las ventanas teselan
  y flotan SIN tapar la barra ni la consola de la cima.
- `consola`: tipos `Taskbar` y `CeldaTaskbar`; métodos `pintar_taskbar`
  (fondo + línea divisoria + una pestaña por ventana viva) y
  `pintar_etiqueta` (rasteriza una cadena en un punto fijo, sin tocar la
  pluma de la consola). La pestaña enfocada lleva el índigo del foco; las
  desalojadas, su color de baliza; las demás, el slate del panel.
- `compositor::recomponer` arma la lista de celdas y la pasa a la consola
  junto con las capas: un único repintado, una única presentación.
- `atender_raton`: un clic dentro de la franja de la barra busca su pestaña
  con `celda_taskbar_en` y enfoca la ventana correspondiente (sin iniciar
  arrastre). El comportamiento sobre el área de apps queda intacto.

### Cambiado
- `compositor::fundar` y `nacer_ventana` exigen un nombre para cada ventana.
  `cargar_userspace` lo toma de `EntradaApp.nombre`; `lanzar_app`, de
  `Plantilla.nombre`.

### Verificado
- QEMU. Tras arrancar se ve la barra al pie con las 7 pestañas (`tonada`
  enfocada, índigo), las desalojadas en sus colores de baliza. Un clic
  sobre `pulso` cambia el foco al instante: el borde índigo del compositor
  deja la maestra y envuelve a `pulso`, y la pestaña `pulso` se ilumina.

## Fase 15 — La voz del sistema — 2026-05-23

La bocina pertenecía a la ventana enfocada (Fase 12), pero el kernel necesita
hablar también: un acorde al abrir la casa, un repique al recibir un inquilino
nuevo, un bajo al echar a uno que se rompió. La Fase 15 le da al sistema su
propia voz, prioritaria sobre las de los apps.

### Añadido
- **`altavoz::SECUENCIA`** — una `VecDeque<(u32, u32)>` (frecuencia, duración
  ms) que el kernel encola con `altavoz::agendar(&[(...)])`. `FIN_NOTA` (un
  `AtomicU64`) recuerda el milisegundo del reloj monótono en que la nota
  actual debe terminar.
- **`altavoz::atender()`** — invocada por la tarea del compositor cada
  fotograma; si la nota actual ya terminó, saca la siguiente de la cola y la
  toca; si la cola está vacía, silencia.
- **`altavoz::kernel_sonando()`** — `true` mientras `FIN_NOTA` esté en el
  futuro. `sys_tono` lo consulta y, en ese caso, ignora la llamada del app:
  el kernel no se interrumpe a sí mismo.
- **Catálogo de voces**: `VOZ_BIENVENIDA` (Do5-Mi5-Sol5 ascendente, 500 ms),
  `VOZ_LANZAR` (repique 700→1050 Hz), `VOZ_CERRAR` (descendente 900→520 Hz),
  `VOZ_DESALOJO` (bajo 180 Hz, 260 ms).
- **Hitos sonoros**: `kernel_main` agenda `VOZ_BIENVENIDA` justo antes de
  `ejecutor.run()`. `nacer_ventana` agenda `VOZ_LANZAR`. `cerrar` agenda
  `VOZ_CERRAR`. `desalojar` agenda `VOZ_DESALOJO`.

### Cambiado
- Las pestañas de la barra de tareas calculan su tinta por brillo del fondo
  (ITU-R BT.601): la pestaña amarilla pálida del desalojo por memoria, que
  llevaba texto blanco invisible, ahora luce su nombre en tinta oscura.

### Verificado
- QEMU con `-audiodev wav -machine pcspk-audiodev=spk`. El PCM crudo revela,
  en orden, las tres notas del acorde de bienvenida (≈520, 630, 760 Hz),
  inmediatamente un brevísimo bajo de 180 Hz (la baliza de discola/glotona
  desalojadas), y después la escala de `tonada` tomando la bocina.
- Captura: la pestaña de `glotona` (crema) muestra ahora su nombre legible
  en tinta oscura; la de `discola` (púrpura) sigue clara, como antes.

## Fase 16 — La barra viva: lanzador y reloj — 2026-05-23

La barra de tareas era informativa pero pasiva: nombraba a los inquilinos sin
ofrecerse a ningún gesto propio. La Fase 16 le añade los dos accesorios de
toda barra de tareas digna: un **botón lanzador** a la izquierda y un **reloj**
a la derecha, y le pone el reloj a latir cada segundo.

### Añadido
- **Botón lanzador («+»)** a la izquierda de la barra (36 px de ancho, fondo
  índigo del foco, cruz blanca). Un clic incrementa `PARTOS` — el mismo
  contador que `Alt+N` —, así que la tarea del compositor lo recoge y lanza
  la siguiente app de la rotación. El teclado y el ratón comparten ahora la
  misma vía para crear ventanas.
- **Reloj** a la derecha de la barra (80 px), formato `mm:ss` desde el
  arranque, leído de `reloj::milisegundos()`. Tinta blanca sobre slate.
- **`compositor::tick_reloj()`** — la invoca la tarea del compositor cada
  fotograma. Si el segundo del reloj cambió respecto al último mostrado
  (`ULTIMO_SEGUNDO: AtomicU64`), recompone; si no, vuelve. Cero coste cuando
  no hace falta refrescar.
- `Taskbar` gana los campos `launcher: RegionPantalla`, `reloj: &str` y
  `reloj_region: RegionPantalla`. `pintar_taskbar` los pinta como dos cuñas
  fijas de la barra: el lanzador con su cruz dibujada en píxeles directos
  (independiente de la tipografía), el reloj con la etiqueta de costumbre.

### Cambiado
- El layout de la barra se recalcula: `cells_x0` empieza tras el lanzador,
  `cells_x_max` termina antes del reloj. `celda_taskbar_en` respeta esos
  límites; un clic en el lanzador (`clic_en_launcher`) tiene su propia rama
  en `atender_raton`, antes que la búsqueda de pestaña.
- `CELDA_TASKBAR_ANCHO` baja de 156 a 150 para que el lanzador, las siete
  pestañas y el reloj quepan holgados en una pantalla de 1280 píxeles.

### Verificado
- QEMU. La barra al arrancar muestra el botón «+» indigo a la izquierda, las
  siete pestañas (con `glotona` ya legible en tinta oscura sobre crema), y el
  reloj `0:17` a la derecha (el tiempo que el kernel lleva vivo al capturar).
  Diez segundos después, el reloj marca `0:29` — la barra se ha refrescado
  doce veces sin intervención del ratón ni del teclado.

## Fase 17 — `bitacora` :: el editor que recuerda — 2026-05-23

`memoriosa` (Fase 7c) demostró que un app podía persistir un contador a
través de los reinicios. `bitacora` extiende esa demostración a un EDITOR de
texto: tecleas, los caracteres aparecen; reinicias el kernel, el texto
sigue ahí. La huella vive en el grafo de objetos, como todo lo demás.

### Añadido — app `bitacora` (`apps/bitacora/`)
- **Lienzo 480×280** con título "bitacora :: el texto persiste" en índigo
  arriba y, debajo, las últimas líneas del buffer. Wrap automático a 28
  columnas; `Enter` salta de línea; Backspace borra el último carácter.
- **Tipografía 8×8** embebida (crate `font8x8 = "0.3"`), escalada x2 a 16×16.
  El app rasteriza cada glifo pixel a pixel desde su propia memoria lineal.
- **Persistencia automática**: cada cambio invoca `sys_estado_guardar`. Al
  arrancar, `init` llama a `sys_estado_cargar` y restaura el buffer.
- Buffer de 512 bytes; al desbordarse descarta los 64 primeros para hacer
  hueco (amortiza el coste — no es una mudanza por cada pulsación).
- Mapeo de scancodes US a ASCII para letras, dígitos y puntuación común;
  Enter genera `\n`, Backspace borra. Sin mayúsculas ni modificadores.

### Cambiado
- **`GENESIS` crece de 7 a 8 apps** con `bitacora` como la PRIMERA — gana
  la celda maestra al arrancar, así que la primera ventana grande del
  escritorio te invita a teclear.
- **`CELDA_TASKBAR_ANCHO`** baja de 150 a 130 píxeles para que las ocho
  pestañas + el lanzador + el reloj quepan holgadas en 1280 px.

### Verificado
- QEMU (`sendkey` del monitor): tras escribir `hola renaser` y `quit` →
  relanzar QEMU con el mismo `disk.img`, la `bitacora` muestra de nuevo el
  texto justo donde quedó. El `almacen` reporta 24 objetos en el grafo
  (frente a 9 antes de escribir) y `raiz presente`: cada `guardar` anexó
  una versión al log direccionado por contenido.

## Fase 18 — Red: virtio-net y el primer hola al exterior — 2026-05-23

renaser hablaba consigo mismo. Con la misma plantilla del disco —enumerar
PCI, montar el transporte virtio, ceder a `virtio-drivers` el diálogo de
bajo nivel— y reutilizando el `KernelHal` y el mapeador `memory::mmio`,
el kernel abre ahora una boca y una oreja al exterior: una tarjeta de red.

### Añadido
- **Driver `drivers/red`** — monta `VirtIONet<KernelHal, PciTransport, 16>`,
  enruta su IRQ vía el PIC, expone `enviar(frame)` y `drenar_rx(callback)`.
  La cabeza del archivo guarda el MAC del dispositivo y las constantes de
  IP de QEMU user-mode (`10.0.2.15` invitado, `10.0.2.2` gateway).
- **`componer_arp_request(mac, ip, objetivo)`** — construye un frame
  Ethernet + ARP listo para enviar.
- **`interrupts::registrar_irq_red`** + `extern "x86-interrupt" irq_red`
  — mismo patrón que la IRQ del disco. Acknowledge en el dispositivo,
  EOI al PIC.
- **`KernelHal` se hace `pub`** para que `red` lo reutilice como su Hal
  de DMA — la misma arena de marcos físicos que el disco. El mapeador
  MMIO (Fase 13.5) cubre los BAR del nuevo dispositivo sin tocar nada.
- **`tarea_red`**: tras 10 fotogramas para que la cola RX se estabilice,
  envía un ARP request al gateway de QEMU. Después, cada fotograma drena
  la cola RX y vuelca cada paquete a COM1.
- **QEMU args**: `-netdev user,id=net0 -device virtio-net-pci,netdev=net0`
  (user-mode networking, NAT virtual al host).

### Verificado
- QEMU con `-object filter-dump,...,file=/tmp/red.pcap`. Trazas COM1:
  ```
  red :: virtio-net :: MAC 52:54:00:12:34:56 :: IRQ Some(11)
  red :: ARP REQUEST enviado :: ¿quien tiene 10.0.2.2?
  red :: RX 64 bytes :: dst=52:54:00:12:34:56 src=52:55:0a:00:02:02 type=0x0806
  ```
  El src del paquete entrante codifica la IP del gateway dentro del MAC
  (`52:55:0a:00:02:02` = QEMU prefix + `10.0.2.2`): la respuesta ARP es
  legítima. Renaser ya habla con la red.

---

## Fase 19 — Voz del userspace hacia la red — 2026-05-23

La Fase 18 hizo que el kernel hablara con la red. La Fase 19 le da la
misma capacidad a los apps: tres capacidades nuevas (`sys_net_mac`,
`sys_net_enviar`, `sys_net_recibir`) y una primera app que las usa.

### Añadido
- **`drivers::red::recibir_en(buf) -> usize`** — recibe UN paquete por
  llamada, lo copia al buffer del llamante, recicla el descriptor a la
  cola RX. Devuelve 0 si no hay paquete pendiente. El gemelo cooperativo
  de `drenar_rx`, pensado para la interfaz que el host expone a los apps.
- **Tres capacidades nuevas en `wasm/env`** (índices 12, 13, 14):
  - `sys_net_mac(salida: u32) -> i32` — escribe seis bytes con la MAC del
    dispositivo en `(salida, 6)`. Devuelve `0` OK o `-1` si no hay red.
  - `sys_net_enviar(ptr: u32, len: u32) -> i32` — envía el frame Ethernet
    crudo. Devuelve `0` OK, `-1` red caída, `-2` longitud inválida,
    `-3` rango fuera de la memoria lineal.
  - `sys_net_recibir(salida: u32, capacidad: u32) -> i32` — drena UN
    paquete (si hay) hacia `(salida, capacidad)`. Devuelve los bytes
    copiados, `0` si nada pendiente, `-1` si no hay red, `-2` si la
    capacidad excede la memoria lineal.
  Todas validan `rango()` contra la memoria del módulo y registran
  diagnóstico por COM1 si falla la validación.
- **App `pregon` (`apps/pregon/`, 4.2 KiB WASM)** — la primera voz del
  userspace hacia la red. Al arrancar:
  1. Pide su MAC al host vía `sys_net_mac`.
  2. Si la tiene, anuncia su presencia con un broadcast Ethernet
     (destino `FF:FF:FF:FF:FF:FF`, EtherType experimental `0x88B5`,
     payload ASCII `"renaser :: hola desde mi red"` — 42 bytes en el cable).
  En cada `tick` drena un paquete con `sys_net_recibir` y pinta:
  el título, el MAC propio, las cuentas TX/RX, y los datos del último
  frame entrante (tamaño, EtherType, src MAC) — todo en tipografía 8×8
  embebida (crate `font8x8`), escalada x2 sobre un lienzo 480×160 índigo.
- **`pregon` en el manifiesto de genesis** (`boot/src/main.rs`): la novena
  app, posición 2 (detrás de `bitacora`). El disco se siembra ahora con
  9 entradas; el kernel reporta `manifiesto :: 9 apps nacidas del grafo`.

### Cambiado
- **`tarea_red`** del kernel ya no drena la cola RX: la posesión de la
  cola pasa al userspace vía `sys_net_recibir`. La tarea conserva el
  envío del ARP de prueba al gateway en cuanto el dispositivo se
  estabiliza, y termina.
- **`CELDA_TASKBAR_ANCHO`** baja de 130 a 116 px para que las nueve
  pestañas + lanzador + reloj caben holgadas en 1280 px.
- **`ETHER_TYPE_RENASER`** del driver pasa a `#[allow(dead_code)]` —
  ahora vive en el userspace (en `pregon`); el kernel la conserva como
  referencia para futuras pilas nativas.

### Verificado
- QEMU headless con `-object filter-dump,id=fd,netdev=net0,file=/tmp/renaser.pcap`.
  El pcap captura tres frames en orden:
  ```
  #1 incl=42 dst=ff:ff:ff:ff:ff:ff src=52:54:00:12:34:56 etype=0x88b5
     payload: 'renaser :: hola desde mi red'
  #2 incl=42 dst=ff:ff:ff:ff:ff:ff src=52:54:00:12:34:56 etype=0x0806 (ARP REQ)
  #3 incl=64 dst=52:54:00:12:34:56 src=52:55:0a:00:02:02 etype=0x0806 (ARP REP)
  ```
  El primero es `pregon`; el segundo el kernel; el tercero es el gateway
  respondiéndonos — el ciclo completo de salida y entrada en una sola
  fase.
- Captura de pantalla: la ventana `pregon :: voz hacia la red` aparece
  teselada, su pestaña sale en la barra de tareas entre `bitacora` y
  `tonada`, y la consola anuncia `manifiesto :: 9 apps nacidas del grafo`.

---

## Fase 20 — Akasha Over Ether: el grafo distribuido — 2026-05-23

La red dejo de ser un puro medio para mensajes ad-hoc y se vuelve el cable
por donde el GRAFO de objetos se extiende a otras maquinas renaser. Tres
mensajes bastan; `postcard` los serializa; el EtherType `0x88B5` los
transporta; ningun byte de TCP, IP, DNS o cabecera transporte de los anos 80.

### Añadido
- **Crate `akasha/`** — nucleo `#![no_std]` compartido (gemela de `formato`),
  excluida del workspace anfitrion. Define `MensajeAkasha` con tres
  variantes —`SolicitarObjeto(id)`, `ProveedorObjeto(id, payload)`,
  `AnunciarRaiz(id)`—, las helpers `componer_frame(src, dst, msg)` y
  `analizar_frame(bytes) -> Result<(Mac, MensajeAkasha)>`, y las constantes
  `ETHER_TYPE_AKASHA = 0x88B5`, `MAX_PAYLOAD_AKASHA = 1486`. Seis pruebas
  unitarias cubren el ida-y-vuelta de cada variante, la distincion entre
  EtherType ajeno, frame truncado y payload basura.
- **Modulo `kernel/src/akasha.rs`** — el respondedor del nucleo. Tres oficios:
  1. **Demuxer** (`drenar_y_demultiplexar`): drena la cola RX del dispositivo
     virtio-net y demultiplexa: frames AoE con payload valido los procesa
     `procesar`; el resto va a la cola del userspace.
  2. **Atencion de mensajes** (`procesar`): `SolicitarObjeto` busca en
     `almacen` y, si tiene el objeto, responde `ProveedorObjeto` unicast;
     `ProveedorObjeto` verifica integridad (`blake3(payload) == id`) y
     absorbe al grafo local con `almacen::almacenar`; `AnunciarRaiz` se
     contabiliza y, si no tenemos el nodo, le solicita al emisor.
  3. **Faro** (`difundir_raiz` cada 5 s): difunde por broadcast el hash del
     manifiesto actual — el faro Akasha que delata nuestra presencia en la
     red de capa-2.
- **Cola del userspace** — el demuxer empuja los frames no-AoE a una
  `VecDeque<Vec<u8>>` con profundidad maxima 64; `sys_net_recibir` ahora
  pasa por `akasha::pop_usuario(buf)` en lugar de leer directamente del
  driver. Asi el kernel filtra Akasha sin disputarle paquetes al userspace.
- **`tic_compositor`** — el punto de entrada de los oficios per-fotograma de
  AoE, llamado desde el tic del compositor (cuyo latido es fiable). El
  demuxer corre en cada vuelta; el faro se dispara cuando el reloj monotono
  cruza `INTERVALO_FARO_MS` (5 s).
- **Contadores** (`ResumenAkasha`): `rx_solicitudes`, `rx_proveedores`,
  `rx_anuncios`, `tx_*` equivalentes, `rx_descartados`, `usuario_encolados`,
  `usuario_desbordados`. Quedaran de fundamento para un futuro indicador AoE
  en la barra de tareas.

### Cambiado
- **`tarea_red`** vuelve a su rol simple: enviar UN ARP request al gateway de
  QEMU al arrancar. El demuxer y el faro pasaron al tic del compositor —
  cuyo latido cooperativo es el unico GARANTIZADO bajo carga del reactor.
  Un bucle propio en `tarea_red` (intentado primero) escala mal porque la
  cadencia de `EsperaFrame::await` se degrada cuando los apps WASM bajo
  `wasmi` ocupan el reactor (cada tic de app son decenas de ms de
  interprete); medirlo contra el reloj monotono y enganchar al compositor
  resuelve la falla con limpieza.
- **`sys_net_recibir`** lee de la cola del userspace que mantiene `akasha`,
  no del driver. El bloque doc en `wasm/env.rs` lo refleja.
- **`drivers::red::recibir_en`** queda como primitiva del driver con
  `#[allow(dead_code)]`: ya no la consume el sistema, pero la conservamos
  para futuras herramientas de diagnostico que quieran leer la cola RX cruda.
- **Demuxer politico**: frames `0x88B5` con payload basura para postcard —
  por ejemplo el saludo en texto plano de `pregon`— se cuentan en
  `RX_DESCARTADOS` pero TAMBIEN viajan a la cola del userspace. Asi pregon
  o cualquier otro app que comparta el EtherType experimental pueda
  recibirlos sin que el kernel se los robe.

### Verificado
- `cargo test -p akasha` — seis pruebas en verde:
  ```
  test pruebas::anuncio_de_raiz_viaja_compacto ... ok
  test pruebas::componer_y_analizar_solicitud_es_simetrico ... ok
  test pruebas::ethertype_ajeno_se_distingue ... ok
  test pruebas::frame_demasiado_corto_se_distingue ... ok
  test pruebas::payload_invalido_se_distingue ... ok
  test pruebas::proveedor_lleva_payload_arbitrario ... ok
  ```
- QEMU headless 60 s, `-object filter-dump,file=/tmp/renaser-aoe.pcap`. El
  pcap captura 14 frames:
  ```
  ARP: 2  (request del kernel + reply del gateway)
  pregon: 1  (saludo texto plano)
  AnunciarRaiz: 11  (faros del kernel, Δ promedio 5.86 s sobre 60 s)
  ```
  Cada AnunciarRaiz lleva como payload el hash del manifiesto:
  `2f3deadfcc7dae25..` (postcard de 33 bytes — variante + hash — sobre un
  frame Ethernet total de 47 bytes).
- COM1 vuelca `akasha :: ANUNCIO emitido :: raiz=2f3deadfcc7dae25..` por
  cada disparo; en una corrida de 60 s aparecen 11 anuncios consecutivos.
