khipu: el motor de dependencias inversas que cruza TODAS las colas (y arregla la mentira de perfiles)

El 2026-07-22 un cambio a libdrm (default_library=both, para gtk4) invalidó 118
recetas KDE. El radio se midió contra recipes/*.toml y no contra
recipes/incoming-kde/ ⇒ 8 en vez de 118. Y build-state.py sin --kde cometía el
MISMO error: cargaba sólo el corpus, y su campo `perfiles` AFIRMABA que tocar
libdrm sólo afectaba a escritorio-mirada. Una mentira con confianza.

Causa estructural: las dependencias INVERSAS son un hecho del disco entero, no
de la vista que uno cargó. khipu.py las calcula SIEMPRE sobre todas las colas,
con resolución sibling-first (el nodo se identifica por (cola, nombre), porque
corpus/fontconfig y incoming-kde/fontconfig son nudos distintos).

  QUÉ NODOS PUNTÚO es decisión de vista. QUIÉN ME CONSUME es un hecho.

khipu es la PUERTA ÚNICA de la metodología: radio/perfiles nativos (cruzan todo
el repo) + delega estado/drenar/triaje/frontera/objetivo a sus órganos.

  khipu radio libdrm → 126 directos, 132 transitivos, [escritorio-kde,
                        escritorio-mirada], 11 sellados que caen. El número
                        honesto que habría frenado el cambio.

build-state.py ahora saca `perfiles` y el nuevo `dependientes_total` de khipu
(grafo entero), no de los nodos que cargó. Verificado: el grafo por defecto ya
atribuye libdrm a escritorio-kde; sin_perfil bajó 674→666 (8 recetas del corpus
que sólo KDE consume, ahora bien atribuidas). --check sigue en 0, grafo cierra.

Repo-wide por diseño, no KDE-céntrico: zlib toca las 4 imágenes (233 transitivos),
make 313.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-22 18:43:40 -04:00
co-authored by Claude Opus 4.8
parent ea5b27dba4
commit 8da6ef6122
2 changed files with 252 additions and 10 deletions
+22 -10
View File
@@ -40,6 +40,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import targets as targets_mod
import khipu # el motor de dependencias INVERSAS: cruza TODAS las colas (ver por qué en su cabecera)
ROOT = Path(__file__).resolve().parent.parent
os.chdir(ROOT)
@@ -220,18 +221,29 @@ def main():
r["unblocks"] = sum(1 for m, rm in recs.items()
if rm["state"] in DEBT_STATES and n in rm.get("deps", []))
# MEMBRESÍA DE PERFIL: qué imágenes alcanzan cada nodo. `perfiles: []` = no lo necesita ninguna
# imagen declarada (catálogo, no distro) — el dato incómodo que hasta ahora nadie medía.
for r in recs.values():
r["perfiles"] = []
# MEMBRESÍA DE PERFIL y DEPENDIENTES INVERSOS: los dos se calculan sobre el GRAFO ENTERO (todas
# las colas del disco), NO sobre los nodos que esta vista cargó. Es la corrección del bug de
# libdrm: en la corrida del corpus, `perfiles` llegaba a decir `['escritorio-mirada']` sobre un
# paquete que sostiene KDE entero, porque no veía las recetas de incoming-kde. khipu las ve
# siempre y resuelve sibling-first ⇒ el nodo se identifica por (cola, nombre). Ver khipu.py.
todas = khipu.cargar_colas()
memb = khipu.membresia(todas, cargar_perfiles({q for q in todas})) # TODOS los perfiles, no sólo los de esta vista
rev = khipu.dependientes(todas)
for n, r in recs.items():
clave = (r.get("queue", "corpus"), n)
r["perfiles"] = sorted(memb.get(clave, ()))
# Dependientes inversos DEL GRAFO ENTERO: cuántas recetas (de cualquier cola) me consumen. Es
# el radio real de un re-hash. `unblocks` (abajo) es local a la deuda de esta vista; este no.
r["dependientes_total"] = len(rev.get(clave, ()))
sin_perfil = sum(1 for r in recs.values() if not r["perfiles"])
# `by_profile` (el "cuánto falta" por imagen) SÍ es local: sólo cuenta estados de nodos que esta
# vista cargó ⇒ se limita a los perfiles cuya cola está cargada (los demás darían conteos falsos).
by_profile = {}
for pn, p in sorted(perfiles.items()):
cl = clausura(p["raices"], recs)
for n in sorted(cl):
recs[n]["perfiles"].append(pn)
by_profile[pn] = dict(raices=len(p["raices"]), clausura=len(cl),
**Counter(recs[n]["state"] for n in cl))
sin_perfil = sum(1 for r in recs.values() if not r["perfiles"])
miembros = [n for n, r in recs.items() if pn in r["perfiles"]]
by_profile[pn] = dict(raices=len(p["raices"]), clausura=len(miembros),
**Counter(recs[n]["state"] for n in miembros))
by_state = Counter(r["state"] for r in recs.values())
by_cls = Counter(r["cls"] for r in recs.values() if "cls" in r)