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):