1# ADR 0019 — El dueño del formato Card: tawasuyu lo DEFINE, takana lo GENERA
2
3- **Estado:** PROPUESTO. La dirección se decide acá; **cuándo se paga la adopción** queda abierto y
4 depende de una medición concreta (§Precondiciones, el hash de Stage 1).
5- **Fecha:** 2026-09-14
6- **Frontera:** en takana, `crates/takana-bootstrap/src/lib.rs` (`STAGE1_SEED_CARD`,
7 `SSHD_SERVICE_CARD`), `scripts/mudanza/formatos/arje.py`, `scripts/mudanza/formatos/{systemd,openrc}.py`,
8 `scripts/mudanza/declarar.py`. En tawasuyu, `shared/card/card-core`,
9 `03_ukupacha/arje/arje-card{,-builder}`, `03_ukupacha/arje/init/arje-absorb`.
10- **Relacionado:** [SDD 29](../29-mudanza.md) (la mudanza), [SDD 30](../30-servicios-de-paquete.md)
11 (qué arranca), [ADR 0006](0006-pinned-commits.md) (dependencias por commit).
12
13## Contexto
14
15La mudanza de gioser (SDD 29) produce tarjetas de arje: es como un servicio ajeno —systemd, OpenRC,
16o un proceso que corre y nadie declaró— se convierte en algo que `arje-zero` sabe levantar. El
17producto instalado también las emite: su `seed.card.json` y la card de `sshd` salen de
18`takana-bootstrap`.
19
20Nunca se escribió **quién es dueño de ese formato**, y por omisión hoy son dos repos. Esto ya se pagó
21un piso más abajo: dentro de takana había **dos emisores** del mismo formato (`declarar.py` y
22`formatos/arje.py`) y **cada uno tenía un campo distinto mal en la raíz** — uno mandaba `provides`
23vacío, el otro `supervision: Restart` sobre una card `Virtual`. Ninguno de los dos coincidía con la
24semilla real del producto. Se consolidó en un solo emisor. Este ADR es el mismo problema un piso más
25arriba: **el segundo emisor está en el otro repo**.
26
27## Lo medido (2026-09-14)
28
29| qué | resultado |
30|---|---|
31| 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)»* |
32| …y **valida** su versión | `pub const CARD_SCHEMA_VERSION: u16 = 1` + `if self.schema_version != CARD_SCHEMA_VERSION { … }` |
33| emisores de Card **dentro de takana** | **2**, en dos lenguajes: `takana-bootstrap` (JSON crudo en constantes `r#"…"#`) y `scripts/mudanza/formatos/arje.py` |
34| dependencias de `card-core` / `arje-card` en `crates/*/Cargo.toml` | **0** |
35| sitios donde takana escribe `"schema_version": 1` **a mano** | **5** |
36| `arje-absorb` (tawasuyu) | usa `card_core::Card` directamente y ya trae lectores `systemd.rs`, `openrc.rs`, `runit.rs`, `dinit.rs`, `sysvinit.rs` |
37| `scripts/mudanza/formatos/` (takana) | reimplementa en Python **dos de esos cinco** (`systemd.py`, `openrc.py`) y aporta uno que allá no existe: `proc.py` |
38| `card` como crate | **hoja**: sin deps internas, sólo crates.io ⇒ depender de él es barato |
39| `STAGE1_SEED_CARD` | **entra en `inputs` del hash** del bootstrap (`lib.rs:332`) |
40| el repo `card` | está entre los **26 que sólo existen en gioser**, la máquina que se borra |
41
42El dato que decide: **card-core valida la versión de esquema y takana la escribe a mano en cinco
43sitios.** El día que `CARD_SCHEMA_VERSION` pase a 2, arje rechazará las tarjetas de takana — y el
44fallo no aparece en un test: aparece **en el arranque de una caja recién instalada**, que es el peor
45sitio y el más tarde posible. Hoy coinciden por suerte, no por construcción.
46
47## La pregunta que este ADR contesta
48
49«Integrar tawasuyu» admite dos lecturas, y la respuesta **no es la misma para las dos**:
50
511. **usar piezas de tawasuyu dentro de esta herramienta**, o
522. **que la herramienta viva en tawasuyu y desde acá se invoque**.
53
54> **Para el FORMATO, (1): se adopta el tipo de tawasuyu.
55> Para la HERRAMIENTA, ninguna de las dos: la mudanza es de takana y se queda acá.**
56
57La mudanza habla de recetas, perfiles, store, hidratación y ArtifactHash: su vocabulario es el de
58takana, y mudarla a tawasuyu la dejaría hablando de cosas que ese repo no tiene. Lo que cruza la
59frontera es **el tipo**, no la herramienta.
60
61## Decisión
62
63**1. tawasuyu DEFINE el formato; takana lo GENERA.** `card-core` es la única definición. No se copia,
64no se reimplementa y no se documenta en paralelo. Una copia del tipo es la misma duplicación que este
65ADR cierra, sólo que con la palabra «vendor» delante.
66
67**2. Un solo emisor en takana, y que serialice con el TIPO, no con `format!`.** Un envoltorio fino
68(`takana-card`) sobre `card-core`, usado por `takana-bootstrap` **y** por la mudanza. El criterio de
69aceptación es negativo y comprobable: **cero literales `"schema_version"` en el árbol de takana**.
70
71**3. La mudanza no se muda.** Sigue en `scripts/mudanza/`, y su reescritura a subcomando (`takana
72migrate`) es una decisión de otro ADR — independiente de ésta.
73
74**4. Los lectores de init ajeno se retiran de takana, no se mejoran.** `arje-absorb` ya lee
75systemd/OpenRC/runit/dinit/sysvinit contra el tipo canónico; `formatos/systemd.py` y `openrc.py` son
76una segunda implementación de dos de ellos. Se retiran **cuando** absorb exponga su resultado de
77forma consumible por un tercero; hasta entonces conviven **con una prueba que compare las dos salidas
78sobre la misma entrada y falle si divergen** — porque dos traductores que nadie compara divergen sin
79que nada falle, que es exactamente cómo empezó esto.
80
81**5. Lo que takana aporta va HACIA tawasuyu, no en paralelo.** El lector `proc` —absorber del proceso
82VIVO y no de la declaración— es de takana y no existe allá. Y hace falta: en gioser `rc-status` decía
83`stopped` de cinco servicios **vivos**, así que absorb solo produciría una Semilla perfecta que no
84levanta el servidor. Se ofrece como aportación a `arje-absorb`; si tawasuyu no lo toma, se queda acá
85como lector propio **del tipo de allá**, que sigue siendo una sola definición.
86
87**6. La dirección de dependencia es única: takana → tawasuyu**, por commit pineado (ADR 0006).
88tawasuyu no depende de takana. No es una preferencia: **21 recetas del corpus ya construyen
89componentes de tawasuyu** (`arje-zero`, `mirada-*`, `netup`, `arje-absorb`, `arje-installer`,
90`cosmos-cli`, …). La dependencia inversa cerraría un ciclo entre dos repos que ya se necesitan en un
91sentido.
92
93## Precondiciones que no se saltan
94
95**a. `card` tiene que tener copia fuera de gioser antes de que takana dependa de él.** Es uno de los
9626 repos sin espejo. Hacer que el bootstrap de la distro dependa en tiempo de compilación de un repo
97que **sólo existe en la máquina que estamos por borrar** convierte un problema conocido en un
98bloqueo de arranque. Es el paso 1 del plan de mudanza, y es precondición de esta decisión.
99
100**b. Adoptar el tipo puede mover el baseline del selfhost, y hay que MEDIRLO antes.**
101`STAGE1_SEED_CARD` entra en los `inputs` del hash del bootstrap: si al serializar con `card-core` el
102JSON sale con otro orden de claves, otro espaciado u otro campo por defecto, **el hash de Stage 1 se
103mueve**. Se comprueba con `takana hash` antes y después; si se mueve, la adopción en `takana-bootstrap`
104es una decisión propia (con el selfhost delante) y **no se hace de paso**. La mudanza no tiene ese
105problema: sus cards no entran en ningún hash, así que puede adoptar el tipo primero.
106
107## Consecuencias
108
109**Se gana:** cuando el esquema suba, el fallo pasa de *«una caja recién instalada no arranca»* a
110*«no compila»*. Es el mismo cambio de lugar que buscan la regla 3 del `CLAUDE.md` y el ADR 0012: que
111lo que se rompe, se rompa ruidosamente y temprano.
112
113**Cuesta:** takana suma una dependencia de compilación sobre un repo privado. Es barata en lo técnico
114(`card` es hoja, sólo crates.io) y cara en lo operativo mientras la precondición (a) siga sin cumplir.
115
116**Rechazado — copiar el tipo a takana (vendor):** quita la dependencia y deja exactamente el problema
117que este ADR existe para cerrar, con la divergencia además invisible.
118
119**Rechazado — mover la mudanza a tawasuyu:** el vocabulario de la mudanza es el de takana.
120
121## Lo que NO decide este ADR
122
123- Dónde vive el `[[sitio]]` (la utilidad de vhosts y el versionado de datos por frentes). Es un ADR
124 propio, y la respuesta puede ser distinta que ésta.
125- Si `arje-absorb` incorpora el lector `proc`: es decisión de tawasuyu, no de este repo.
126- Cuándo se paga la adopción en `takana-bootstrap` — depende de la medición (b).
127- Si la mudanza se reescribe como subcomando de takana.
0019-dueno-del-formato-card.md
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 (la mudanza), SDD 30
(qué arranca), ADR 0006 (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:
- usar piezas de tawasuyu dentro de esta herramienta, o
- 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.