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 1347Shell 123Rust 78Markdown 712325 Sutichaykuna

⎇ d33fc94fc59e581842d785cb3fff35b73f4b0abe scripts/drenar.py Python · 199 siq'ikuna · 9.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

gnome: enciende la CAPA GRAFO del yupana-gnome d33fc94fc · sergio ·

1#!/usr/bin/env python3
2# drenar.py — el ORDEN DE MASTICADO: qué construir, en qué orden, para acercar una imagen a estar
3# completa. (P4 del plan docs/plan-catalogo-objetivo.md.)
4#
5# POR QUÉ. La cola de la granja era AUTORÍA: 55 ficheros en `tandas/` escritos a mano, y para la
6# deuda una lista que el hub medía con `saldar-deuda-static.sh` y pegaba en `campana-deuda.sh`. Pero
7# el grafo de estado ya sabe qué falta (`state`), qué lo traba (`blocked_by`) y a cuántos destraba
8# (`unblocks`). Este script convierte eso en la cola: DERIVADA, no escrita.
9#
10# ONDAS. No lista "lo que falta" en un montón: lo parte en ondas topológicas. Onda 1 = construible
11# YA (ninguna dep en deuda). Onda 2 = lo que la onda 1 destraba. Etc. Eso da tres cosas que una
12# lista plana no da: el orden correcto, la PROFUNDIDAD real de la cadena (cuántos pasos secuenciales
13# faltan, que es lo que manda en el reloj de pared), y qué se puede paralelizar sin pisarse.
14# Dentro de cada onda ordena por `unblocks` desc: primero lo que libera más.
15#
16# LA REGLA DE LA FUENTE ÚNICA. Un nodo `wanted` (sin receta) no tiene deps propias; se le miran las
17# SEMBRADAS (`seed-edges.json`, P3). En cuanto tiene receta, manda la receta — ya corregida por el
18# veredicto de harkaq. La semilla es andamio y se cae sola.
19#
20# NO LANZA NADA. Sólo imprime/escribe la cola; construir cuesta € y es decisión explícita
21# (`farm-up` + `campana-deuda.sh`). Ver el pie de la salida.
22#
23# Uso:  scripts/drenar.py --perfil escritorio-kde       # ondas de una imagen
24#       scripts/drenar.py                               # todo el corpus en deuda
25#       scripts/drenar.py --perfil cli --n 20           # sólo las 20 primeras construibles ya
26#       scripts/drenar.py --perfil escritorio-kde --tanda tandas/kde-drenaje.txt
27import json, sys
28from pathlib import Path
29
30ROOT = Path(__file__).resolve().parent.parent
31GRAFOS = ["docs/state/build-state.json", "docs/state/build-state-kde.json",
32          "docs/state/build-state-gnome.json"]
33SEMILLA = ROOT / "docs/state/seed-edges.json"
34EN_DEUDA = ("debt", "never", "unhashable", "wanted")
35
36
37def cargar(perfil):
38    """Elige el grafo por la COLA que el perfil declara en `targets.toml`, no por cuál tiene más
39    nodos. La diferencia importa: la cola `incoming-kde` SOMBREA recetas canónicas, así que medir
40    `escritorio-mirada` contra el grafo KDE mide una imagen que nadie construye. Cada perfil se mide
41    contra el árbol de recetas del que realmente sale."""
42    sys.path.insert(0, str(ROOT / "scripts"))
43    import targets as targets_mod
44    cola = "corpus"
45    if perfil is not None:
46        try:
47            cola = targets_mod.load().get(perfil, {}).get("cola", "corpus")
48        except Exception:
49            pass
50    for f in GRAFOS:
51        p = ROOT / f
52        if not p.exists():
53            continue
54        d = json.loads(p.read_text())
55        if cola not in (d.get("by_queue") or {}):
56            continue
57        if perfil is None or any(perfil in (r.get("perfiles") or []) for r in d["nodes"].values()):
58            return d, f
59    return None, None
60
61
62def deps_de(nombre, nodo, semilla):
63    """La regla de la fuente única: con receta manda la receta; sin receta (nodo `wanted`) se usan
64    las aristas SEMBRADAS, que son hipótesis y se marcan como tales."""
65    if nodo["state"] != "wanted":
66        return nodo.get("deps") or [], "receta"
67    aristas = (semilla.get("nodos") or {}).get(nombre)
68    if isinstance(aristas, list):
69        return [a["dep"] for a in aristas if a.get("clase") in ("conocida", "frontera")], "semilla"
70    return [], "sin-datos"
71
72
73def ondas_de(perfil):
74    """(ondas, deuda, ambito, grafo) — el cálculo puro, sin imprimir. Reutilizado por --todos."""
75    doc, cual = cargar(perfil)
76    if doc is None:
77        return None, None, None, None
78    semilla = json.loads(SEMILLA.read_text()) if SEMILLA.exists() else {}
79    nodos = doc["nodes"]
80    ambito = {n: r for n, r in nodos.items()
81              if perfil is None or perfil in (r.get("perfiles") or [])}
82    deuda = {n: r for n, r in ambito.items() if r["state"] in EN_DEUDA}
83    restante, ondas = dict(deuda), []
84    while restante:
85        onda = [n for n, r in restante.items()
86                if not any(d in restante for d in deps_de(n, r, semilla)[0])]
87        if not onda:
88            ondas.append(sorted(restante)); break
89        onda.sort(key=lambda n: (-restante[n].get("unblocks", 0), restante[n].get("cls", ""), n))
90        ondas.append(onda)
91        for n in onda:
92            del restante[n]
93    return ondas, deuda, ambito, cual
94
95
96def todos_los_perfiles():
97    """Artefacto versionado con el drenaje de CADA imagen. El `git diff` entre dos corridas es el
98    avance real: qué salió de la deuda y qué onda se vació. Lo regenera el latido de la granja."""
99    sys.path.insert(0, str(ROOT / "scripts"))
100    import targets as targets_mod
101    doc = {"schema": "hammer-drenaje/1",
102           "nota": "DERIVADO del grafo de estado por scripts/drenar.py — no editar a mano.",
103           "perfiles": {}}
104    for p in sorted(targets_mod.load()):
105        ondas, deuda, ambito, cual = ondas_de(p)
106        if ondas is None:
107            continue
108        doc["perfiles"][p] = dict(
109            grafo=cual, nodos=len(ambito), en_deuda=len(deuda), ondas=len(ondas),
110            profundidad=len(ondas), construibles_ya=ondas[0] if ondas else [],
111            olas=[{"n": i + 1, "recetas": o} for i, o in enumerate(ondas)])
112        print(f"   {p:20} {len(deuda):3} en deuda · {len(ondas):2} ondas · "
113              f"onda 1 = {len(ondas[0]) if ondas else 0}")
114    salida = ROOT / "docs/state/drenaje.json"
115    salida.write_text(json.dumps(doc, indent=1, ensure_ascii=False) + "\n")
116    print(f"   → {salida.relative_to(ROOT)}")
117    return 0
118
119
120def main():
121    args = sys.argv[1:]
122    if "--todos" in args:
123        return todos_los_perfiles()
124    perfil = args[args.index("--perfil") + 1] if "--perfil" in args else None
125    tope = int(args[args.index("--n") + 1]) if "--n" in args else None
126    tanda = args[args.index("--tanda") + 1] if "--tanda" in args else None
127
128    doc, cual = cargar(perfil)
129    if doc is None:
130        print(f"no encontré un grafo con el perfil `{perfil}` "
131              f"(¿corriste scripts/build-state.py [--kde]?)", file=sys.stderr)
132        return 1
133    semilla = json.loads(SEMILLA.read_text()) if SEMILLA.exists() else {}
134    nodos = doc["nodes"]
135
136    ambito = {n: r for n, r in nodos.items()
137              if perfil is None or perfil in (r.get("perfiles") or [])}
138    deuda = {n: r for n, r in ambito.items() if r["state"] in EN_DEUDA}
139    etiqueta = f"perfil `{perfil}`" if perfil else "corpus entero"
140    print(f"== drenaje de {etiqueta} ({cual})")
141    print(f"   {len(ambito)} nodos en ámbito · {len(deuda)} en deuda "
142          f"({len(ambito)-len(deuda)} ya sellados)")
143    if not deuda:
144        print("   nada que drenar: la imagen está completa.")
145        return 0
146
147    # Ondas topológicas sobre el subgrafo EN DEUDA: una receta sólo espera por deps que también
148    # estén en deuda (las selladas ya están en el store y no bloquean).
149    restante = dict(deuda)
150    ondas, procedencia = [], {}
151    while restante:
152        onda = []
153        for n, r in restante.items():
154            ds, origen = deps_de(n, r, semilla)
155            procedencia[n] = origen
156            if not any(d in restante for d in ds):
157                onda.append(n)
158        if not onda:  # ciclo entre nodos en deuda: no debería pasar (topo_ok), pero no se oculta
159            print(f"\n   ⚠ CICLO entre {len(restante)} nodos en deuda: "
160                  f"{' '.join(sorted(restante)[:10])}")
161            ondas.append(sorted(restante))
162            break
163        onda.sort(key=lambda n: (-restante[n].get("unblocks", 0), restante[n].get("cls", ""), n))
164        ondas.append(onda)
165        for n in onda:
166            del restante[n]
167
168    print(f"\n== {len(ondas)} ondas (profundidad real de la cadena; el reloj de pared manda acá)")
169    for i, onda in enumerate(ondas, 1):
170        muestra = onda if tope is None or i > 1 else onda[:tope]
171        cab = f"   onda {i}: {len(onda):3} construibles"
172        if i == 1:
173            cab += "  ← ARRANCÁ POR ACÁ (nada las bloquea)"
174        print(cab)
175        for n in muestra[:25]:
176            r = deuda[n]
177            u = r.get("unblocks", 0)
178            marca = f" [{procedencia[n]}]" if procedencia[n] != "receta" else ""
179            print(f"        {n:28} {r['state']:10} destraba {u:3}  {r.get('cls','')}{marca}")
180        if len(muestra) > 25:
181            print(f"        … y {len(muestra)-25} más")
182
183    primera = ondas[0][:tope] if tope else ondas[0]
184    print(f"\n== cola para el worker ({len(primera)} recetas de la onda 1)")
185    print("   " + " ".join(primera))
186    if tanda:
187        p = ROOT / tanda
188        p.parent.mkdir(parents=True, exist_ok=True)
189        p.write_text(f"# DERIVADA de {cual} por scripts/drenar.py — no editar a mano.\n"
190                     f"# {etiqueta}: onda 1 de {len(ondas)}, ordenada por `unblocks` desc.\n"
191                     + "\n".join(primera) + "\n")
192        print(f"   → {tanda}")
193    print("\n   construir NO es gratis (€): esto no lanza nada. En el worker:")
194    print(f"     scripts/farm/campana-deuda.sh {' '.join(primera[:6])}…")
195    return 0
196
197
198if __name__ == "__main__":
199    sys.exit(main())