From 748b2f604c601133349062cbf8f6baefb9a5dfaf Mon Sep 17 00:00:00 2001 From: Sergio Date: Sat, 5 Sep 2026 16:31:31 +0000 Subject: [PATCH] =?UTF-8?q?yupana:=20el=20khipu=20se=20descubre,=20no=20se?= =?UTF-8?q?=20escribe=20a=20mano=20=E2=80=94=20cosmic=20y=20wlr=20eran=20i?= =?UTF-8?q?nvisibles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dos sitios llevaban la lista de grafos codificada como `(kde, gnome, corpus)` y **omitían `build-state-cosmic.json` y `build-state-wlr.json`**, que son dos de los cuatro escritorios. Es exactamente la forma del fallo que motivó este fichero y que su cabecera describe: medir contra un subconjunto de las colas y que el número salga chico sin que nada avise. Medido: `_estado()` devolvía `?` para los 36 nudos propios de cosmic y para 12 de los 14 de wlr. Pero el daño peor no era el `?`. `_estado()` elegía el fichero por la cola —gnome si `incoming-gnome`, corpus si `corpus`, **KDE para todo lo demás**— así que un nudo de `incoming-wlr` se buscaba en el grafo de KDE. `gdk-pixbuf` existe en `corpus` Y en `incoming-wlr` con recetas y hashes distintos, y la consulta por el de wlr devolvía el estado del de corpus, con toda confianza. Coincidía en `sealed`, así que nunca se notó. Un `?` se ve; una respuesta de la receta equivocada, no. Ahora `_khipus()` los descubre por glob (un grafo nuevo entra solo; ninguno se puede olvidar) y `_estados_por_cola()` indexa por **(cola, nombre)**, que es la clave real: el mismo nombre vive en varias colas siendo recetas distintas. Si la cola no tiene ese nudo, la respuesta es `?` — que es la verdad, y no el estado de su homónimo. `keystones` compartía el mismo índice por nombre suelto: pasa a `(cola, nombre)` en las tres consultas. Los tres invariantes de `test-yupana-radio.py` siguen verdes, y las 50 discrepancias de cosmic+wlr bajan a 0. --- scripts/yupana.py | 57 ++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/scripts/yupana.py b/scripts/yupana.py index 5804477e..0e957079 100755 --- a/scripts/yupana.py +++ b/scripts/yupana.py @@ -358,6 +358,37 @@ def _dominador_retencion(colas, perfiles): return ret +def _khipus(): + """TODOS los grafos de estado del disco, descubiertos y no escritos a mano. + + La lista estaba codificada como `(kde, gnome, corpus)` en dos sitios y **omitía cosmic y wlr**, + que son dos de los cuatro escritorios. Es la misma forma del fallo que motivó este fichero (ver + la cabecera): medir contra un subconjunto de las colas y que el número salga chico sin que nada + avise. Un grafo nuevo entra solo; ninguno se puede olvidar. + """ + return sorted(glob.glob(str(ROOT / "docs/state/build-state*.json"))) + + +def _estados_por_cola(): + """`{(cola, nombre): estado}` sobre todos los khipus. + + Va por (COLA, nombre) y no por nombre: el mismo nombre vive en varias colas con recetas y hashes + DISTINTOS —`gdk-pixbuf` está en corpus, en incoming-kde y en incoming-wlr— así que un índice por + nombre devuelve el estado de otra receta con toda confianza. Eso es peor que no saber. + """ + estados = {} + for f in _khipus(): + try: + nodos = json.loads(Path(f).read_text())["nodes"] + except Exception: + continue + for n, r in nodos.items(): + q = r.get("queue") + if q: + estados.setdefault((q, n), r.get("state", "?")) + return estados + + def keystones(como_json=False): """Los nudos que GATEAN más trabajo. Métrica accionable: cierre inverso transitivo restringido a la DEUDA (cuánto bloqueado libero al sellar esto). Segunda columna: RETENCIÓN por dominadores (lo @@ -366,23 +397,17 @@ def keystones(como_json=False): colas = cargar_colas() rev = dependientes(colas) perfiles = targets_mod.load() - # estados desde el khipu más completo - estados = {} - for f in ("docs/state/build-state-kde.json", "docs/state/build-state-gnome.json", - "docs/state/build-state.json"): - p = ROOT / f - if p.exists(): - for n, r in json.loads(p.read_text())["nodes"].items(): - estados.setdefault(n, r.get("state", "?")) + # estados desde TODOS los khipus, indexados por (cola, nombre) + estados = _estados_por_cola() DEUDA = ("debt", "never", "unhashable", "wanted") ret = _dominador_retencion(colas, perfiles) filas = [] for nodo in list(rev): cerr = _cierre_inverso(nodo, rev) - en_deuda = sum(1 for (q, n) in cerr if estados.get(n) in DEUDA) + en_deuda = sum(1 for nd in cerr if estados.get(nd) in DEUDA) q, n = nodo - yo_deuda = estados.get(n) in DEUDA + yo_deuda = estados.get(nodo) in DEUDA filas.append(dict(nudo=f"{q}/{n}", gateo_total=len(cerr), gateo_en_deuda=en_deuda, exclusivo=ret.get(nodo, 0), en_deuda=yo_deuda)) # KEYSTONE ACCIONABLE: un nodo SELLADO ya está disponible ⇒ construirlo no libera nada (make gatea @@ -397,7 +422,7 @@ def keystones(como_json=False): # camino crítico = nº de pasos (lo que ya daban las ondas). Con datos, es la ETA real en segundos. tiempos = _tiempos() idx = {q: set(m) for q, m in colas.items()} - deuda_set = {(q, n) for q in colas for n in colas[q] if estados.get(n) in DEUDA} + deuda_set = {(q, n) for q in colas for n in colas[q] if estados.get((q, n)) in DEUDA} deps_debt = {} for (q, n) in deuda_set: dd = set() @@ -640,15 +665,7 @@ def membresia(colas=None, perfiles=None): def _estado(nodo): """Estado del nudo desde el grafo firme, si está regenerado. Best-effort: el radio funciona sin él.""" - q, nombre = nodo - fichero = ("docs/state/build-state-gnome.json" if q == "incoming-gnome" - else "docs/state/build-state-kde.json" if q != "corpus" - else "docs/state/build-state.json") - try: - d = json.loads((ROOT / fichero).read_text()) - return d["nodes"].get(nombre, {}).get("state", "?") - except Exception: - return "?" + return _estados_por_cola().get(nodo, "?") def radio(nombre, como_json=False):