yupana: el khipu se descubre, no se escribe a mano — cosmic y wlr eran invisibles

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.
This commit is contained in:
Sergio
2026-09-05 16:31:31 +00:00
parent 66db079b64
commit 748b2f604c
+37 -20
View File
@@ -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):