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 85Rust 842918 Sutichaykuna

catálogo objetivo P1: manifiesto de perfiles — el set de la distro deja de ser un string de shell

El destino de la distro vivía en 2 strings de product-userland-from-repo.sh, 1
de mirada-usb.sh y 55 tandas planas. Ahora en docs/state/targets.toml: un perfil
= una imagen enviable, listando sólo las RAÍCES (lo que se pide por nombre); la
clausura la calcula el grafo, no un humano.

4 perfiles: base (26), cli (46, hereda base), escritorio-mirada (14),
escritorio-kde (7 raíces, cola incoming-kde). scripts/targets.py expande hereda
(transitivo, con detección de ciclo) preservando el orden — mirada lo necesita.

Lift-and-shift verificado: las 3 listas expandidas son byte-idénticas a los
strings que reemplazan. Ninguna imagen cambia de contenido.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

3a64b603d7b59a732f06d0b63beafff32849007c · ruwaq sergio · (3 killa ñawpaq) · tayta c963b24b5 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 3

scripts/targets.py

4 qillqakuna · +153 −7

diff --git a/docs/state/targets.toml b/docs/state/targets.toml
new file mode 100644
index 00000000..598befe4
--- /dev/null
+++ b/docs/state/targets.toml
@@ -0,0 +1,61 @@
+# targets.toml — el MANIFIESTO DE OBJETIVO: qué paquetes debe tener la distro.
+#
+# POR QUÉ EXISTE. `build-state.json` es el grafo de lo que EXISTE (768 recetas, cierra sin huérfanas).
+# Lo que la distro DEBE tener no estaba en ningún lado: vivía en dos strings de shell de
+# `product-userland-from-repo.sh`, otro de `mirada-usb.sh`, 55 listas planas en `tandas/` y una cola
+# de 206 recetas KDE. Por eso "¿cuánto falta?" no tenía respuesta: el mapa cerraba perfecto, pero
+# sólo cubría el territorio ya conquistado. Este fichero es el destino. (Plan: docs/plan-catalogo-objetivo.md)
+#
+# LA INVERSIÓN. `paquetes` lista sólo las RAÍCES — lo que un usuario pide POR NOMBRE. La clausura
+# (las libs de las que cuelgan) la calcula el grafo siguiendo las aristas de deps, no la escribe un
+# humano. Al revés de `tandas/base-system-3-libs.txt`, que enumera a mano miembros de la clausura.
+#
+# Un `perfil` = una imagen enviable. `hereda` compone (transitivo, sin ciclos). `cola` dice en qué
+# árbol de recetas viven sus raíces: `corpus` = `recipes/`; otra cosa = `recipes/incoming-<x>/`, que
+# `build-state.py` sólo carga con su flag (`--kde`) ⇒ los perfiles de una cola no cargada se saltan
+# en vez de reportar raíces fantasma.
+#
+# Consumidores: `scripts/targets.py <perfil>` (expande a lista de nombres) y `scripts/build-state.py`
+# (marca los nodos `wanted` y calcula la membresía de perfil de cada receta).
+schema = "hammer-targets/1"
+
+[perfil.base]
+descripcion = "userland foundational C: la distro arranca y se usa (shell, VCS, privilegios, red, disco, TLS, proceso, editor, docs)"
+# Lift verbatim de BASE_SYSTEM_PKGS (scripts/product-userland-from-repo.sh). De-Alpinizado y firmado,
+# tandas base-system-1..3.
+paquetes = [
+  "bash", "git", "sudo", "doas", "util-linux", "shadow", "kbd", "procps-ng",
+  "iproute2", "iputils", "dhcpcd", "e2fsprogs", "dosfstools", "parted", "rsync",
+  "sed", "tzdata", "vim", "dbus", "ca-certificates", "gnupg", "lsof", "strace",
+  "pciutils", "usbutils", "mandoc",
+]
+
+[perfil.cli]
+descripcion = "la capa CLI moderna (Rust) encima de base — el userland real del producto"
+hereda = ["base"]
+# Lift verbatim de CLI_RUST_PKGS (scripts/product-userland-from-repo.sh).
+paquetes = [
+  "bat", "fd", "ripgrep", "jq", "zoxide", "dust", "procs", "htop", "less", "tree",
+  "hyperfine", "delta", "eza", "sd", "tokei", "fzf", "skim", "git-cliff", "gitui",
+  "starship",
+]
+
+[perfil.escritorio-mirada]
+descripcion = "rootfs slim del USB mirada: compositor Wayland propio sobre mesa software"
+# Lift verbatim de PKGS (scripts/mirada-usb.sh). NO hereda base: es una imagen slim a propósito.
+# El ORDEN importa (libs primero, binarios mirada al final) y se preserva.
+paquetes = [
+  "mesa-swrast", "wayland", "libdrm", "seatd", "libinput", "libudev-zero",
+  "libxkbcommon", "pixman", "linux-pam", "mtdev", "libevdev",
+  "mirada-compositor", "mirada-greeter", "mirada-ctl",
+]
+
+[perfil.escritorio-kde]
+descripcion = "escritorio KDE Plasma 6 desde fuente"
+cola = "incoming-kde"
+# Raíces del rootfs de metal (docs/HANDOFF-noche-kde-2026-07-14.md:135). El resto del escritorio
+# —137 artefactos hidratados— es CLAUSURA de estas 7, no lista a mano.
+paquetes = [
+  "plasma-workspace", "kwin", "kscreenlocker", "breeze", "frameworkintegration",
+  "qqc2-desktop-style", "plasma-integration",
+]
diff --git a/scripts/mirada-usb.sh b/scripts/mirada-usb.sh
index 0312d2d3..52227a95 100644
--- a/scripts/mirada-usb.sh
+++ b/scripts/mirada-usb.sh
@@ -34,8 +34,9 @@ declare -A PIN=(
   [mesa-swrast]=410e4fb9806598921048f492508725f55ee8475a1e9ef8ea2dc64f357154a0d9
   [libxkbcommon]=cef7db4a82882ee942fef5e758344da67c9f3fce6b221610a7db9cb05bfb3b53
 )
-# Orden: libs primero, binarios mirada al final. Todos aportan .so o /usr/bin.
-PKGS="mesa-swrast wayland libdrm seatd libinput libudev-zero libxkbcommon pixman linux-pam mtdev libevdev mirada-compositor mirada-greeter mirada-ctl"
+# Orden: libs primero, binarios mirada al final. Todos aportan .so o /usr/bin. El set sale del
+# MANIFIESTO DE OBJETIVO (docs/state/targets.toml, perfil `escritorio-mirada`), que preserva el orden.
+PKGS="$("$(dirname "$0")/targets.py" escritorio-mirada)"
 
 resolve() { # nombre -> hash del store (pin si existe, si no el más reciente por mtime)
   local n="$1"
diff --git a/scripts/product-userland-from-repo.sh b/scripts/product-userland-from-repo.sh
index ff650d96..685a4210 100755
--- a/scripts/product-userland-from-repo.sh
+++ b/scripts/product-userland-from-repo.sh
@@ -18,11 +18,12 @@ DB="$RFS/var/lib/hammer/installed.json"
 
 # Set curado por defecto: el userland "real" que hace usable a la distro. Todos deben estar en el
 # índice del repo firmado. Editá/pasá args para cambiarlo.
-# base-system: userland foundational C (shell/VCS/privilegios/red/disco/TLS/proceso/editor/docs),
-# de-Alpinizado y firmado (tandas base-system-1..3). CLI Rust: la capa "moderna" curada encima.
-BASE_SYSTEM_PKGS="bash git sudo doas util-linux shadow kbd procps-ng iproute2 iputils dhcpcd e2fsprogs dosfstools parted rsync sed tzdata vim dbus ca-certificates gnupg lsof strace pciutils usbutils mandoc"
-CLI_RUST_PKGS="bat fd ripgrep jq zoxide dust procs htop less tree hyperfine delta eza sd tokei fzf skim git-cliff gitui starship"
-DEFAULT_PKGS="$BASE_SYSTEM_PKGS $CLI_RUST_PKGS"
+# El set ya NO vive acá: sale del MANIFIESTO DE OBJETIVO (docs/state/targets.toml), perfil `cli` =
+# `base` (userland foundational C, de-Alpinizado, tandas base-system-1..3) + la capa CLI Rust encima.
+# Cambiar el userland del producto = editar el manifiesto, que es también lo que mide el grafo de
+# estado. Env PERFIL para armar otra imagen (`scripts/targets.py --lista` los muestra).
+PERFIL="${PERFIL:-cli}"
+DEFAULT_PKGS="$("$ROOT/scripts/targets.py" "$PERFIL")"
 PKGS_LIST="${PKGS:-${*:-$DEFAULT_PKGS}}"
 
 [ -n "${OVER:-}" ] || { rm -rf "$RFS"; mkdir -p "$RFS"; }
diff --git a/scripts/targets.py b/scripts/targets.py
new file mode 100755
index 00000000..2139e130
--- /dev/null
+++ b/scripts/targets.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+# targets.py — lee `docs/state/targets.toml` (el manifiesto de objetivo) y expande un perfil a su
+# lista de RAÍCES. Un solo parser para dos consumidores: los scripts de imagen (shell) y el grafo
+# de estado (`build-state.py`), para que el set de paquetes de la distro no vuelva a vivir en tres
+# strings de shell distintos.
+#
+# Uso:  scripts/targets.py cli               # raíces expandidas, una línea separada por espacios
+#       scripts/targets.py --lista           # perfiles disponibles, con su descripción
+#       scripts/targets.py --json            # todo el manifiesto expandido
+# Como módulo:  from targets import load  →  {perfil: {descripcion, cola, hereda, paquetes, raices}}
+#
+# `hereda` es transitivo y el orden se PRESERVA (primero las raíces de lo heredado, en el orden
+# declarado, después las propias), porque hay imágenes donde el orden de hidratación importa
+# (mirada: libs antes que binarios). Duplicados: se queda la primera aparición.
+import json, sys, tomllib
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+DEFAULT = ROOT / "docs/state/targets.toml"
+
+
+def load(path=DEFAULT):
+    """Devuelve {perfil: {descripcion, cola, hereda, paquetes, raices}} con `raices` ya expandido."""
+    doc = tomllib.load(open(path, "rb"))
+    crudos = doc.get("perfil", {})
+    perfiles = {}
+    for n, p in crudos.items():
+        perfiles[n] = dict(
+            descripcion=p.get("descripcion", ""),
+            cola=p.get("cola", "corpus"),
+            hereda=list(p.get("hereda", [])),
+            paquetes=list(p.get("paquetes", [])),
+        )
+
+    # Expansión de `hereda` con detección de ciclo (un manifiesto con un ciclo es un error del
+    # humano, no un caso a tolerar: sale ruidoso).
+    def expandir(n, camino):
+        if n in camino:
+            raise ValueError(f"ciclo en `hereda`: {' → '.join(camino + [n])}")
+        if n not in perfiles:
+            raise KeyError(f"perfil desconocido en `hereda`: {n}")
+        p = perfiles[n]
+        if "raices" in p:
+            return p["raices"]
+        vistos, raices = set(), []
+        for h in p["hereda"] + [None]:
+            for pkg in (expandir(h, camino + [n]) if h else p["paquetes"]):
+                if pkg not in vistos:
+                    vistos.add(pkg); raices.append(pkg)
+        p["raices"] = raices
+        return raices
+
+    for n in perfiles:
+        expandir(n, [])
+    return perfiles
+
+
+def main():
+    args = sys.argv[1:]
+    try:
+        perfiles = load()
+    except FileNotFoundError:
+        print(f"no existe {DEFAULT.relative_to(ROOT)}", file=sys.stderr); return 1
+    except (ValueError, KeyError) as e:
+        print(f"manifiesto inválido: {e}", file=sys.stderr); return 1
+
+    if not args or args[0] in ("--lista", "-l"):
+        for n, p in sorted(perfiles.items()):
+            print(f"{n:20} {len(p['raices']):3} raíces  [{p['cola']}]  {p['descripcion']}")
+        return 0
+    if args[0] == "--json":
+        print(json.dumps(perfiles, indent=1, ensure_ascii=False)); return 0
+
+    n = args[0]
+    if n not in perfiles:
+        print(f"perfil desconocido: {n} (hay: {', '.join(sorted(perfiles))})", file=sys.stderr)
+        return 1
+    print(" ".join(perfiles[n]["raices"]))
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())