ADR 0019: el dueño del formato Card — y takana lo escribe a mano en cinco sitios

La mudanza emite tarjetas de arje y el producto instalado también; quién es dueño del formato no
estaba escrito, así que por omisión son dos repos. Medido hoy:

  el tipo canónico existe y está en tawasuyu   `shared/card/card-core::Card`
  …y VALIDA su versión                          CARD_SCHEMA_VERSION = 1, comparada al deserializar
  emisores dentro de takana                     2, en dos lenguajes (bootstrap Rust + arje.py)
  deps de card-core en crates/*/Cargo.toml      0
  `"schema_version": 1` escrito a mano          5 sitios
  arje-absorb                                   usa card_core::Card y ya trae systemd/openrc/runit/
                                                dinit/sysvinit; formatos/ reimplementa DOS en Python

El día que el esquema pase a 2, arje rechaza las tarjetas de takana — y no falla en un test: falla
en el arranque de una caja recién instalada. Hoy coinciden por suerte, no por construcción. Es el
mismo fallo que ya se pagó un piso más abajo, cuando había dos emisores DENTRO de takana y cada uno
tenía un campo distinto mal en la raíz.

Decisión: tawasuyu DEFINE, takana GENERA. Un solo emisor acá y que serialice con el tipo, no con
`format!` — criterio de aceptación negativo: cero literales `"schema_version"` en el árbol. Los
lectores de init ajeno se RETIRAN (absorb ya los tiene), no se mejoran, y mientras convivan hay una
prueba que compara las dos salidas y falla si divergen. Lo que takana aporta —el lector `proc`, que
lee lo VIVO porque `rc-status` miente— va hacia absorb, no en paralelo.

Y la herramienta NO se muda a tawasuyu: la mudanza habla de recetas, perfiles, store y ArtifactHash.
Lo que cruza la frontera es el tipo, no la herramienta.

Dos precondiciones que no se saltan: `card` es uno de los 26 repos sin copia fuera de gioser (hacer
que el bootstrap dependa en compilación de un repo que sólo vive en la máquina que se borra es
convertir un problema conocido en un bloqueo de arranque); y `STAGE1_SEED_CARD` ENTRA EN EL HASH del
bootstrap, así que reserializar con card-core puede mover el baseline del selfhost — se mide con
`takana hash` antes, y si se mueve es una decisión propia. La mudanza no tiene ese problema y puede
adoptar el tipo primero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
This commit is contained in:
Sergio
2026-09-14 15:01:05 +00:00
co-authored by Claude Opus 5
parent 76535f8e42
commit 447e151424
+127
View File
@@ -0,0 +1,127 @@
# ADR 0019 — El dueño del formato Card: tawasuyu lo DEFINE, takana lo GENERA
- **Estado:** PROPUESTO. La dirección se decide acá; **cuándo se paga la adopción** queda abierto y
depende de una medición concreta (§Precondiciones, el hash de Stage 1).
- **Fecha:** 2026-09-14
- **Frontera:** en takana, `crates/takana-bootstrap/src/lib.rs` (`STAGE1_SEED_CARD`,
`SSHD_SERVICE_CARD`), `scripts/mudanza/formatos/arje.py`, `scripts/mudanza/formatos/{systemd,openrc}.py`,
`scripts/mudanza/declarar.py`. En tawasuyu, `shared/card/card-core`,
`03_ukupacha/arje/arje-card{,-builder}`, `03_ukupacha/arje/init/arje-absorb`.
- **Relacionado:** [SDD 29](../29-mudanza.md) (la mudanza), [SDD 30](../30-servicios-de-paquete.md)
(qué arranca), [ADR 0006](0006-pinned-commits.md) (dependencias por commit).
## Contexto
La mudanza de gioser (SDD 29) produce tarjetas de arje: es como un servicio ajeno —systemd, OpenRC,
o un proceso que corre y nadie declaró— se convierte en algo que `arje-zero` sabe levantar. El
producto instalado también las emite: su `seed.card.json` y la card de `sshd` salen de
`takana-bootstrap`.
Nunca se escribió **quién es dueño de ese formato**, y por omisión hoy son dos repos. Esto ya se pagó
un piso más abajo: dentro de takana había **dos emisores** del mismo formato (`declarar.py` y
`formatos/arje.py`) y **cada uno tenía un campo distinto mal en la raíz** — uno mandaba `provides`
vacío, el otro `supervision: Restart` sobre una card `Virtual`. Ninguno de los dos coincidía con la
semilla real del producto. Se consolidó en un solo emisor. Este ADR es el mismo problema un piso más
arriba: **el segundo emisor está en el otro repo**.
## Lo medido (2026-09-14)
| qué | resultado |
|---|---|
| el tipo canónico existe, y está en tawasuyu | `shared/card/card-core::Card`. `arje-card` se describe a sí mismo: *«Alias histórico de card-core. Re-exporta tipos legacy (EntityCard ≡ Card)»* |
| …y **valida** su versión | `pub const CARD_SCHEMA_VERSION: u16 = 1` + `if self.schema_version != CARD_SCHEMA_VERSION { … }` |
| emisores de Card **dentro de takana** | **2**, en dos lenguajes: `takana-bootstrap` (JSON crudo en constantes `r#"…"#`) y `scripts/mudanza/formatos/arje.py` |
| dependencias de `card-core` / `arje-card` en `crates/*/Cargo.toml` | **0** |
| sitios donde takana escribe `"schema_version": 1` **a mano** | **5** |
| `arje-absorb` (tawasuyu) | usa `card_core::Card` directamente y ya trae lectores `systemd.rs`, `openrc.rs`, `runit.rs`, `dinit.rs`, `sysvinit.rs` |
| `scripts/mudanza/formatos/` (takana) | reimplementa en Python **dos de esos cinco** (`systemd.py`, `openrc.py`) y aporta uno que allá no existe: `proc.py` |
| `card` como crate | **hoja**: sin deps internas, sólo crates.io ⇒ depender de él es barato |
| `STAGE1_SEED_CARD` | **entra en `inputs` del hash** del bootstrap (`lib.rs:332`) |
| el repo `card` | está entre los **26 que sólo existen en gioser**, la máquina que se borra |
El dato que decide: **card-core valida la versión de esquema y takana la escribe a mano en cinco
sitios.** El día que `CARD_SCHEMA_VERSION` pase a 2, arje rechazará las tarjetas de takana — y el
fallo no aparece en un test: aparece **en el arranque de una caja recién instalada**, que es el peor
sitio y el más tarde posible. Hoy coinciden por suerte, no por construcción.
## La pregunta que este ADR contesta
«Integrar tawasuyu» admite dos lecturas, y la respuesta **no es la misma para las dos**:
1. **usar piezas de tawasuyu dentro de esta herramienta**, o
2. **que la herramienta viva en tawasuyu y desde acá se invoque**.
> **Para el FORMATO, (1): se adopta el tipo de tawasuyu.
> Para la HERRAMIENTA, ninguna de las dos: la mudanza es de takana y se queda acá.**
La mudanza habla de recetas, perfiles, store, hidratación y ArtifactHash: su vocabulario es el de
takana, y mudarla a tawasuyu la dejaría hablando de cosas que ese repo no tiene. Lo que cruza la
frontera es **el tipo**, no la herramienta.
## Decisión
**1. tawasuyu DEFINE el formato; takana lo GENERA.** `card-core` es la única definición. No se copia,
no se reimplementa y no se documenta en paralelo. Una copia del tipo es la misma duplicación que este
ADR cierra, sólo que con la palabra «vendor» delante.
**2. Un solo emisor en takana, y que serialice con el TIPO, no con `format!`.** Un envoltorio fino
(`takana-card`) sobre `card-core`, usado por `takana-bootstrap` **y** por la mudanza. El criterio de
aceptación es negativo y comprobable: **cero literales `"schema_version"` en el árbol de takana**.
**3. La mudanza no se muda.** Sigue en `scripts/mudanza/`, y su reescritura a subcomando (`takana
migrate`) es una decisión de otro ADR — independiente de ésta.
**4. Los lectores de init ajeno se retiran de takana, no se mejoran.** `arje-absorb` ya lee
systemd/OpenRC/runit/dinit/sysvinit contra el tipo canónico; `formatos/systemd.py` y `openrc.py` son
una segunda implementación de dos de ellos. Se retiran **cuando** absorb exponga su resultado de
forma consumible por un tercero; hasta entonces conviven **con una prueba que compare las dos salidas
sobre la misma entrada y falle si divergen** — porque dos traductores que nadie compara divergen sin
que nada falle, que es exactamente cómo empezó esto.
**5. Lo que takana aporta va HACIA tawasuyu, no en paralelo.** El lector `proc` —absorber del proceso
VIVO y no de la declaración— es de takana y no existe allá. Y hace falta: en gioser `rc-status` decía
`stopped` de cinco servicios **vivos**, así que absorb solo produciría una Semilla perfecta que no
levanta el servidor. Se ofrece como aportación a `arje-absorb`; si tawasuyu no lo toma, se queda acá
como lector propio **del tipo de allá**, que sigue siendo una sola definición.
**6. La dirección de dependencia es única: takana → tawasuyu**, por commit pineado (ADR 0006).
tawasuyu no depende de takana. No es una preferencia: **21 recetas del corpus ya construyen
componentes de tawasuyu** (`arje-zero`, `mirada-*`, `netup`, `arje-absorb`, `arje-installer`,
`cosmos-cli`, …). La dependencia inversa cerraría un ciclo entre dos repos que ya se necesitan en un
sentido.
## Precondiciones que no se saltan
**a. `card` tiene que tener copia fuera de gioser antes de que takana dependa de él.** Es uno de los
26 repos sin espejo. Hacer que el bootstrap de la distro dependa en tiempo de compilación de un repo
que **sólo existe en la máquina que estamos por borrar** convierte un problema conocido en un
bloqueo de arranque. Es el paso 1 del plan de mudanza, y es precondición de esta decisión.
**b. Adoptar el tipo puede mover el baseline del selfhost, y hay que MEDIRLO antes.**
`STAGE1_SEED_CARD` entra en los `inputs` del hash del bootstrap: si al serializar con `card-core` el
JSON sale con otro orden de claves, otro espaciado u otro campo por defecto, **el hash de Stage 1 se
mueve**. Se comprueba con `takana hash` antes y después; si se mueve, la adopción en `takana-bootstrap`
es una decisión propia (con el selfhost delante) y **no se hace de paso**. La mudanza no tiene ese
problema: sus cards no entran en ningún hash, así que puede adoptar el tipo primero.
## Consecuencias
**Se gana:** cuando el esquema suba, el fallo pasa de *«una caja recién instalada no arranca»* a
*«no compila»*. Es el mismo cambio de lugar que buscan la regla 3 del `CLAUDE.md` y el ADR 0012: que
lo que se rompe, se rompa ruidosamente y temprano.
**Cuesta:** takana suma una dependencia de compilación sobre un repo privado. Es barata en lo técnico
(`card` es hoja, sólo crates.io) y cara en lo operativo mientras la precondición (a) siga sin cumplir.
**Rechazado — copiar el tipo a takana (vendor):** quita la dependencia y deja exactamente el problema
que este ADR existe para cerrar, con la divergencia además invisible.
**Rechazado — mover la mudanza a tawasuyu:** el vocabulario de la mudanza es el de takana.
## Lo que NO decide este ADR
- Dónde vive el `[[sitio]]` (la utilidad de vhosts y el versionado de datos por frentes). Es un ADR
propio, y la respuesta puede ser distinta que ésta.
- Si `arje-absorb` incorpora el lector `proc`: es decisión de tawasuyu, no de este repo.
- Cuándo se paga la adopción en `takana-bootstrap` — depende de la medición (b).
- Si la mudanza se reescribe como subcomando de takana.