gioser

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 1532Shell 196Rust 108Markdown 993686 Definiciones

planear: buscar la receta por el PAQUETE del origen — y el intérprete no es el programa

`origen_binario` buscaba la receta por el nombre del SERVICIO, y ése casi nunca es el nombre del
paquete: `sshd` lo trae `openssh`, `crond` lo trae `cronie`. El censo ya le preguntó al gestor de
paquetes quién posee cada binario, así que ese nombre también se prueba. El perfil pasó de 4 recetas
a 7, y los 27 servicios quedan: receta-takana 9 · suelto 9 · paquete-ajeno 13 · interprete 4 ·
borrado 4.

**La trampa, que es la que haría mentir al perfil.** `openclaw` lo posee el paquete `nodejs`;
`fail2ban-server`, `glances` y `uvicorn` los posee `python`. Contarlos como cubiertos porque existe
`recipes/nodejs.toml` haría salir el perfil N/N describiendo un servidor al que le faltan CUATRO
programas. Tienen clase propia (`interprete`) y cuentan las DOS cosas a la vez, porque las dos son
ciertas: su runtime entra al perfil —`openclaw` necesita `nodejs` en la imagen pase lo que pase— y el
servicio sigue listado como NO cubierto. Meterlo sólo en las raíces miente; dejarlo sólo en los
faltantes arma una imagen sin runtime y el programa, cuando llegue, no arranca.

Y el paquete del origen no se llama igual que la receta ni para el mismo intérprete: Artix empaqueta
`python` y el catálogo tiene `python3.toml`. Sin ese alias tres servicios decían «hace falta una
receta takana» teniendo el runtime sellado — dos trabajos muy distintos.

**Sellada y sin sellar tampoco son lo mismo**, y el perfil las listaba igual: una sellada se INSTALA
del repo firmado, una sin sellar hay que CONSTRUIRLA. Salió con un caso real — `recipes/qdrant.toml`
entró al catálogo desde otro frente mientras se escribía esto y no tiene artefacto. Ahora se marca en
la línea y se resume al pie.

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

053e5e7edf35d658bdbf3bb194d1039f77ace4a1 · por Sergio · (hace 1 mes) · padre fb3a326df · ver el árbol en este commit

Delta semántico · 2

scripts/mudanza/declarar.py

  • cambió por dentro fn perfil

scripts/mudanza/planear.py

3 archivos · +96 −8

diff --git a/docs/29-mudanza.md b/docs/29-mudanza.md
index 4308b9f3..563cbd28 100644
--- a/docs/29-mudanza.md
+++ b/docs/29-mudanza.md
@@ -301,6 +301,42 @@ reconstruyen el servidor desde cero**:
 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.0 duodecies · La cuenta de lo que falta, bien hecha
+
+`origen_binario` buscaba la receta por el **nombre del servicio**, y ése casi nunca es el nombre del
+paquete: `sshd` lo trae `openssh`, `crond` lo trae `cronie`. El censo ya le preguntó al gestor de
+paquetes quién posee cada binario, así que ese nombre también se prueba. Con eso el perfil pasó de
+**4 recetas a 7**, y la clasificación de los 27 servicios quedó así:
+
+```
+  receta-takana  9      paquete-ajeno 13
+  suelto         9      interprete     4      borrado 4
+```
+
+#### La trampa: el intérprete NO es el programa
+
+`openclaw` lo posee el paquete `nodejs`, y `fail2ban-server`, `glances` y `uvicorn` los posee
+`python`. Contarlos como cubiertos porque existe `recipes/nodejs.toml` haría salir el perfil **N/N
+describiendo un servidor al que le faltan cuatro programas**. Tienen clase propia, `interprete`, y
+cuentan **las dos cosas a la vez**, porque las dos son ciertas:
+
+- su **runtime entra al perfil** — `openclaw` necesita `nodejs` en la imagen pase lo que pase;
+- el **servicio sigue listado como no cubierto**, con su motivo.
+
+Meterlo sólo en las raíces haría un perfil que miente; dejarlo sólo en los faltantes armaría una
+imagen sin runtime, y el programa, cuando llegue, no arrancaría.
+
+(Y el paquete del origen no se llama igual que la receta ni para el mismo intérprete: Artix
+empaqueta `python`, el catálogo tiene `python3.toml`. Sin ese alias, tres servicios decían «hace
+falta una receta takana» teniendo el runtime sellado — dos trabajos muy distintos.)
+
+#### Sellada y sin sellar no son lo mismo
+
+El perfil las listaba igual. Una sellada se **instala** del repo firmado; una sin sellar hay que
+**construirla**, y eso puede ser una tarde. Salió con un caso real: `recipes/qdrant.toml` entró al
+catálogo desde otro frente **mientras se escribía esto** y no tiene artefacto. Ahora el perfil lo
+marca en la línea y lo resume al pie.
+
 ### 4.0 undecies · Cuatro binarios que ya no existen — el paso que CADUCA
 
 El más urgente de todo el censo, y lo encontró la lista de lo que el perfil no puede cubrir:
diff --git a/scripts/mudanza/declarar.py b/scripts/mudanza/declarar.py
index cd649631..49e8e1c8 100755
--- a/scripts/mudanza/declarar.py
+++ b/scripts/mudanza/declarar.py
@@ -53,7 +53,7 @@ def perfil(censo, label, repo):
 
     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 = {}, []
+    raices, sin_receta, sin_sellar = {}, [], set()
     for s_ in censo.get("servicio", []):
         if s_.get("decision") != "muda":
             continue
@@ -66,8 +66,25 @@ def perfil(censo, label, repo):
             # (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
+            # ⚠ Sellada y sin sellar no son lo mismo, y el perfil las listaba igual. Una sellada se
+            # INSTALA del repo firmado; una sin sellar hay que CONSTRUIRLA antes, y eso puede ser una
+            # tarde. Medido: `qdrant` entró al catálogo ayer desde otro frente y no tiene artefacto.
+            if "sin sellar" in detalle:
+                sin_sellar.add(receta)
             raices.setdefault(receta, []).append((s_["name"], tuple(s_.get("ports", []))))
         else:
+            # ⚠ Un servicio de clase `interprete` cuenta las DOS cosas a la vez, y hay que hacer las
+            # dos: su runtime SÍ entra al perfil —`openclaw` necesita `nodejs` en la imagen pase lo
+            # que pase— y el servicio sigue listado como NO CUBIERTO, porque el intérprete no es el
+            # programa. Meterlo sólo en las raíces haría salir el perfil N/N con la app faltando;
+            # dejarlo sólo en la lista de faltantes armaría una imagen sin runtime y el programa,
+            # cuando llegue, no arrancaría.
+            if clase == "interprete":
+                import re as _re
+                m = _re.search(r'`recipes/([^`]+)\.toml`', detalle)
+                if m:
+                    raices.setdefault(m.group(1), []).append(
+                        (f"{s_['name']} (sólo el runtime)", tuple(s_.get("ports", []))))
             sin_receta.append((s_["name"], clase, detalle))
 
     L = [f"[perfil.{label}]",
@@ -82,9 +99,14 @@ def perfil(censo, label, repo):
         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 "")
+        if receta in sin_sellar:
+            det = ("⚠ SIN SELLAR: hay que construirla antes de armar la imagen — " + det).strip(" —")
         nota = f"  # {det}" if det and det != receta else ""
         L.append(f'  "{receta}",{nota}')
     L.append("]")
+    if sin_sellar:
+        L.append(f"# ⚠ {len(sin_sellar)} sin artefacto en el store: {', '.join(sorted(sin_sellar))}.")
+        L.append("#   El perfil es correcto, pero armar la imagen las va a CONSTRUIR, no instalar.")
     return "\n".join(L), sin_receta
 
 
diff --git a/scripts/mudanza/planear.py b/scripts/mudanza/planear.py
index ab406277..46980729 100755
--- a/scripts/mudanza/planear.py
+++ b/scripts/mudanza/planear.py
@@ -121,6 +121,17 @@ def _no_aplica(nombre):
     return None
 
 
+# Paquetes que proveen un RUNTIME, no un programa. Que exista `recipes/nodejs.toml` no significa que
+# `openclaw` esté cubierto: el intérprete se resuelve y el programa sigue faltando.
+INTERPRETES = {"python", "python3", "nodejs", "node", "perl", "ruby", "php", "lua", "java", "jre"}
+
+# El paquete del origen y la receta del catálogo no siempre se llaman igual ni siquiera para el mismo
+# intérprete: Artix empaqueta `python` y el catálogo tiene `python3.toml`. Sin esto, tres servicios
+# salían «hace falta una receta takana» cuando el runtime YA está sellado y lo que falta es el
+# programa — dos trabajos muy distintos.
+ALIAS_INTERPRETE = {"python": "python3", "node": "nodejs", "jre": "java"}
+
+
 # ── familias de equivalentes funcionales ────────────────────────────────────────────────────────
 # Agrupa cosas que hacen EL MISMO trabajo, para poder ofrecer alternativas. Es conocimiento de
 # dominio, no gusto: que nginx y caddy sirvan HTTP es un hecho. Lo que NO se decide acá es cuál es
@@ -285,14 +296,33 @@ def origen_binario(e, repo=None):
         # contestar `suelto` a todo sería una respuesta falsa con forma de respuesta.
         raise SystemExit(f"!! no encuentro el catálogo de recetas en {repo}/recipes — "
                          f"pasá --repo o corré desde el repo")
-    for cand in (nombre, nombre.replace("_", "-"), os.path.basename(exe or "")):
-        if cand and os.path.exists(os.path.join(repo, "recipes", f"{cand}.toml")):
-            import glob
+    # ⚠ El nombre del SERVICIO no es el nombre del paquete: `sshd` lo trae `openssh`, `crond` lo trae
+    # `cronie`. Buscar sólo por el nombre del servicio daba «no hay receta» para cinco que el catálogo
+    # SÍ construye y tiene selladas. El censo ya le preguntó al gestor de paquetes quién posee el
+    # binario, así que ese nombre también se prueba.
+    import glob
+    for cand in (nombre, nombre.replace("_", "-"), os.path.basename(exe or ""),
+                 pkg, (pkg or "").removesuffix("-bin"), ALIAS_INTERPRETE.get(pkg or "", "")):
+        if not cand or not os.path.exists(os.path.join(repo, "recipes", f"{cand}.toml")):
+            continue
+        # ⚠⚠ Y la trampa, que es la que haría mentir al perfil: un paquete INTÉRPRETE provee el
+        # runtime, no el programa. `openclaw` lo posee `nodejs` y `fail2ban-server` lo posee
+        # `python`; tener esas recetas NO te da esos servicios. Contarlos como cubiertos haría que el
+        # perfil saliera N/N describiendo un servidor al que le faltan cuatro programas.
+        if pkg in INTERPRETES and cand in (pkg, ALIAS_INTERPRETE.get(pkg or "", "")) and cand != nombre:
             sellado = bool(glob.glob(os.path.join(repo, "store", f"*-{cand}")))
-            return ("receta-takana",
-                    f"takana YA lo construye: `recipes/{cand}.toml`"
-                    + (" (sellado en el store)" if sellado else " (sin sellar: hay que construirlo)")
-                    + f" ⇒ instalarlo del repo firmado, no copiar el binario de {exe or '?'}")
+            return ("interprete",
+                    f"lo corre el intérprete `{pkg}`, que takana SÍ construye (`recipes/{cand}.toml`"
+                    + (", sellada)" if sellado else ", sin sellar)")
+                    + f" — pero el intérprete NO es el programa: `{nombre}` es código que hay que "
+                    f"llevar y declarar aparte. El perfil resuelve el runtime y nada más")
+        sellado = bool(glob.glob(os.path.join(repo, "store", f"*-{cand}")))
+        via = "" if cand in (nombre, nombre.replace("_", "-")) else f" (por su paquete `{pkg}`)" \
+            if cand in (pkg, (pkg or "").removesuffix("-bin")) else f" (por su binario)"
+        return ("receta-takana",
+                f"takana YA lo construye{via}: `recipes/{cand}.toml`"
+                + (" (sellado en el store)" if sellado else " (sin sellar: hay que construirlo)")
+                + f" ⇒ instalarlo del repo firmado, no copiar el binario de {exe or '?'}")
     if pkg:
         return ("paquete-ajeno",
                 f"lo provee el gestor del origen: paquete `{pkg}`. En el destino hace falta una "