tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1411Shell 141Markdown 84Rust 832898 Sutichaykuna

mudanza: el perfil — el software del servidor nuevo se DECLARA, no se instala a mano

`declarar.py --perfil-out` emite un `[perfil.<label>]` listo para `targets.toml`, generado desde el
censo: cada raíz está ahí porque un servicio que CORRÍA la necesita y takana tiene receta.

Por qué un perfil y no una lista de `install`: mudar servicio por servicio a una caja viva va contra
el diseño —el software de una máquina se declara y viene en la imagen, reproducible y firmado— y un
servidor armado a fuerza de instalaciones sueltas no se puede volver a construir. Que es justo el
problema que esta mudanza existe para no repetir: el `caddy` de gioser no tiene dueño ni receta y se
pierde con la máquina.

Y hay un impedimento medido, no estético: `takana install` REPRODUCE desde fuente y eso exige el lab
entero en el cliente (SDD 28 §5.4), que una caja de destino no tiene. El perfil lo resuelve por el
lado correcto: el software entra al armar la imagen, en el hub, que sí tiene lab.

Del censo de gioser salen `caddy`, `gitea`, `python3` (agrupado: lo piden python3 y uvicorn).
Verificado que el instrumental lo acepta: `targets.py gioser-mudado` expande a 33 raíces.

⚠ Y dice lo que NO puede cubrir, uno por uno con su motivo: 34 servicios entre `suelto`,
`paquete-ajeno` y `desconocido`. Un perfil que se calla lo que le falta sale N/N describiendo un
servidor incompleto — la lección de `foot` en escritorio-sway.

Con esto la mudanza produce TRES documentos declarativos que reconstruyen el servidor desde cero:
el perfil (qué software), la semilla (qué corre y cómo) y el plan (qué datos, en qué orden). Ninguno
es un log de lo hecho: los tres son entradas que se vuelven a ejecutar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x

68fdd9eb72906521f872278d432ebd8a69c77667 · ruwaq Sergio · (1 killa ñawpaq) · tayta cc5aebd8c · kay commitpi sach'ata qhaway

Semántico sayk'uy · 2

scripts/mudanza/declarar.py

2 qillqakuna · +122 −1

diff --git a/docs/29-mudanza.md b/docs/29-mudanza.md
index 565d7254..8ad3b3f2 100644
--- a/docs/29-mudanza.md
+++ b/docs/29-mudanza.md
@@ -251,6 +251,56 @@ recomendación misma.
 Si se elige un equivalente, queda en el censo (`alternativa = "…"`) y **el plan lo dice en su paso**,
 con la advertencia arriba de todo: la configuración del origen no sirve tal cual.
 
+### 4.0 quater · El perfil: el software del servidor nuevo se DECLARA, no se instala
+
+`declarar.py --perfil-out` emite además un `[perfil.<label>]` listo para `docs/state/targets.toml`.
+
+**Por qué un perfil y no una lista de `install`.** Mudar servicio por servicio a una caja viva es lo
+que uno haría en cualquier distro, y en takana va contra el diseño: el software de una máquina se
+**declara** y viene en la imagen, reproducible y firmado. Un servidor armado a fuerza de
+instalaciones sueltas no se puede volver a construir — que es exactamente el problema que esta
+mudanza existe para no repetir: **el `caddy` de gioser no tiene dueño ni receta, y se pierde con la
+máquina**.
+
+Y hay un impedimento medido, no estético: `takana install` **reproduce desde fuente**, y eso exige el
+LAB entero en el cliente ([SDD 28 §5.4](28-servidor-de-produccion.md)). Una caja de destino no lo
+tiene. El perfil esquiva el problema por el lado correcto: el software entra **al armar la imagen**,
+en el hub, que sí tiene lab.
+
+Generado desde el censo de gioser:
+
+```toml
+[perfil.gioser-mudado]
+descripcion = "servicios mudados desde momento — generado por scripts/mudanza/declarar.py"
+hereda = ["base"]
+paquetes = [
+  "caddy",
+  "gitea",
+  "python3",  # python3, uvicorn
+]
+```
+
+Verificado que el instrumental lo acepta: `scripts/targets.py gioser-mudado` expande a **33 raíces**.
+
+⚠ **Y dice lo que NO puede cubrir**, uno por uno con su motivo: 34 servicios entre `suelto`,
+`paquete-ajeno` y `desconocido`. *Un perfil que se calla lo que le falta sale N/N describiendo un
+servidor incompleto* — la lección de `foot` en `escritorio-sway`, que llegó a 121/121 sellado y sin
+emulador de terminal.
+
+### 4.1.bis Los tres artefactos declarativos
+
+Con esto la mudanza deja de ser una secuencia de comandos y pasa a producir **tres documentos que
+reconstruyen el servidor desde cero**:
+
+| artefacto | qué declara | lo consume |
+|---|---|---|
+| `[perfil.<x>]` → `targets.toml` | **qué software** lleva | `servidor-image.sh` al armar la imagen |
+| `seed.card.json` | **qué corre y cómo** | `arje-zero` al arrancar |
+| `plan.toml` | **qué datos y en qué orden** | `aplicar.py`, o un humano línea por línea |
+
+Ninguno de los tres es un log de lo que se hizo: los tres son entradas que se vuelven a ejecutar.
+Ésa es la diferencia entre mudar un servidor y poder mudarlo otra vez.
+
 ### 4.1 El aplicador *(implementado: `scripts/mudanza/aplicar.py`)*
 
 `aplicar.py --plan plan.toml [--dry-run] [--only <clase>] [--paso N] [--hecho N]`
diff --git a/scripts/mudanza/declarar.py b/scripts/mudanza/declarar.py
index a64a42bd..a371385c 100755
--- a/scripts/mudanza/declarar.py
+++ b/scripts/mudanza/declarar.py
@@ -31,7 +31,10 @@ variables de entorno va a necesitar que alguien las escriba. Se avisa por nombre
 Uso:
     scripts/mudanza/declarar.py --censo censo.toml [--label mi-servidor] --out seed.card.json
 """
-import argparse, hashlib, json, shlex, sys, tomllib
+import argparse, hashlib, json, os, shlex, sys, tomllib
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from planear import origen_binario, alternativas, _REPO   # noqa: E402
 
 CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"   # sin I, L, O, U
 
@@ -85,11 +88,63 @@ def tarjeta(nombre, cmdline, cwd, puertos):
     }
 
 
+def perfil(censo, label, repo):
+    """Emite un `[perfil.<label>]` para `docs/state/targets.toml`.
+
+    ── POR QUÉ UN PERFIL Y NO UNA LISTA DE `install` ────────────────────────────────────────────
+    Mudar servicio por servicio a una caja viva es lo que uno haría en cualquier distro, y en takana
+    es ir contra el diseño: **el software de una máquina se DECLARA en `targets.toml` y viene en la
+    imagen**, reproducible y firmado. Un servidor armado a fuerza de instalaciones sueltas no se
+    puede volver a construir — que es exactamente el problema que esta mudanza existe para no repetir
+    (el caddy puesto a mano en gioser no tiene dueño ni receta: se pierde con la máquina).
+
+    Además hay un impedimento medido: `takana install` REPRODUCE desde fuente, y eso exige el LAB
+    entero en el cliente (SDD 28 §5.4). Una caja de destino no lo tiene. El perfil esquiva el
+    problema por el lado correcto: el software entra al armar la imagen, en el hub, que sí tiene lab.
+
+    Lo que el perfil NO puede cubrir se lista aparte, con su motivo. Un perfil que se calla lo que le
+    falta sale N/N y describe un servidor incompleto — la lección de `foot` en `escritorio-sway`."""
+    raices, sin_receta = {}, []
+    for s_ in censo.get("servicio", []):
+        if s_.get("decision") != "muda":
+            continue
+        elegido = s_.get("alternativa") or s_["name"]
+        clase, detalle = origen_binario(dict(s_, name=elegido), repo)
+        if clase == "receta-takana":
+            import re as _re
+            m = _re.search(r'`recipes/([^`]+)\.toml`', detalle)
+            # Se agrupa POR RECETA, no por servicio: dos servicios pueden necesitar la misma
+            # (uvicorn y python3 son los dos `python3`), y repetir una raíz en un perfil no la
+            # duplica — sólo ensucia y hace creer que son dos cosas distintas.
+            receta = m.group(1) if m else elegido
+            raices.setdefault(receta, []).append((s_["name"], tuple(s_.get("ports", []))))
+        else:
+            sin_receta.append((s_["name"], clase, detalle))
+
+    L = [f"[perfil.{label}]",
+         f'descripcion = "servicios mudados desde {censo.get("maquina",{}).get("hostname","?")} '
+         f'— generado por scripts/mudanza/declarar.py"',
+         'hereda = ["base"]',
+         "# Generado desde un CENSO, no escrito a mano: cada raíz está acá porque un servicio que",
+         "# CORRÍA en la máquina vieja la necesita, y takana tiene receta para construirla.",
+         "paquetes = ["]
+    for receta in sorted(raices):
+        quienes = raices[receta]
+        nombres = ", ".join(sorted({q[0] for q in quienes}))
+        puertos = sorted({p for q in quienes for p in q[1]})
+        det = nombres + (f" · puertos {','.join(str(x) for x in puertos)}" if puertos else "")
+        nota = f"  # {det}" if det and det != receta else ""
+        L.append(f'  "{receta}",{nota}')
+    L.append("]")
+    return "\n".join(L), sin_receta
+
+
 def main():
     ap = argparse.ArgumentParser(description="Genera una Semilla de arje desde un censo decidido.")
     ap.add_argument("--censo", required=True)
     ap.add_argument("--label", default="takana-servidor", help="etiqueta de la Semilla raíz")
     ap.add_argument("--out", help="fichero de salida (default: stdout)")
+    ap.add_argument("--perfil-out", help="además, escribir un [perfil.<label>] para targets.toml")
     a = ap.parse_args()
 
     with open(a.censo, "rb") as f:
@@ -150,6 +205,22 @@ def main():
         print("     al primer arranque, la rota deja el servicio caído sin que nada avise.")
     print("   ⚠ El entorno (`envp`) va VACÍO: el censo no lee /proc/<pid>/environ porque trae")
     print("     tokens. Un servicio que dependa de variables va a necesitar que alguien las escriba.")
+    if a.perfil_out:
+        bloque, sin_receta = perfil(censo, a.label, _REPO)
+        open(a.perfil_out, "w").write(bloque + "\n")
+        n_r = len([l for l in bloque.splitlines() if l.strip().startswith('"')])
+        print(f"\n══ PERFIL '{a.label}' ══")
+        print(f"   {n_r} receta(s) que takana YA construye → {a.perfil_out}")
+        print("   Pegalo en docs/state/targets.toml y armá la imagen: el software del servidor nuevo")
+        print("   se DECLARA, no se instala a mano. Además `takana install` reproduce desde fuente y")
+        print("   eso exige el lab entero en el cliente — el perfil lo resuelve en el hub, que sí lo tiene.")
+        if sin_receta:
+            print(f"\n   ⚠ {len(sin_receta)} servicio(s) que el perfil NO puede cubrir:")
+            for nom, cl, det in sin_receta:
+                print(f"     {nom:22} [{cl}] {det[:84]}")
+            print("     Un perfil que se calla lo que le falta sale N/N describiendo un servidor")
+            print("     incompleto. Para cada uno: escribir la receta, elegir un equivalente que sí")
+            print("     esté, o llevar el binario a mano sabiendo que no se reproduce.")
     if a.out:
         open(a.out, "w").write(txt)
         print(f"\n   semilla → {a.out}")