estado: orden de ataque por impacto de desbloqueo (lectura accionable del DAG)

El grafo ahora computa, sobre las dependencias, dos campos por receta:
  blocked_by — deps que están en deuda (lo que impide construirla ya)
  unblocks   — cuántas recetas EN DEUDA la declaran como dep (su impacto de desbloqueo)

Una receta en deuda sin blocked_by es construible YA; ordenadas por unblocks desc, dan el orden que
libera el grafo más rápido. La vista muestra la pastilla ↑N en las filas listas y ordena por ahí.

Top de impacto (todo el stack GUI concentrado, más zstd/perl de C-base):
  glib ↑14  wayland ↑13  pixman ↑12  libdrm ↑11  fontconfig ↑11  zstd ↑10  libxkbcommon ↑9  perl ↑8

Es la guía para cuando se levante la granja: construir esas primero desbloquea el grueso.

De paso, diagnóstico de las 8 'never' (ninguna es cruft ni bug): dwarves BLOQUEADA-documentada
(necesita libdw, elfutils da sólo libelf a propósito — sub-proyecto elfutils-libdw); llimphi-counter
es un EJEMPLO/plantilla intencional; las otras 6 son imports Go/Rust que van al worker. Y confirmado
parseando: 0 recetas con FIXME real en el campo sha256 (el grep decía 43, todas comentarios) — otra
vez grep miente, el grafo (parsea) dice la verdad.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 11:48:28 -04:00
co-authored by Claude Opus 4.8
parent e19fb6347f
commit a30e0d3146
4 changed files with 2862 additions and 1035 deletions
+19 -11
View File
@@ -25,16 +25,18 @@ STATE_ES = {"sealed": "sellado al día", "debt": "deuda de rebuild",
"never": "nunca construida", "unhashable": "ilegible"}
CLASS_ES = {"c": "C", "go": "Go", "rust": "Rust", "gui": "GUI", "kernel": "kernel"}
# Para cada nodo, sus deps que están en deuda (lo que lo bloquea) — el sub-grafo accionable.
# blocked_by / unblocks los computa build-state.py sobre el grafo; acá sólo los leemos.
def blockers(name):
r = nodes[name]
return [d for d in r.get("deps", []) if d in nodes and nodes[d]["state"] in ("debt", "never", "unhashable")]
return nodes[name].get("blocked_by", [])
def unblocks(name):
return nodes[name].get("unblocks", 0)
debt = {n: r for n, r in nodes.items() if r["state"] in ("debt", "never", "unhashable")}
# Orden: primero las "listas" (sin bloqueadores), luego por nº de bloqueadores, luego alfabético.
# Orden de ATAQUE: primero las "listas" (sin bloqueadores), y entre ellas la que DESBLOQUEA más
# arriba; luego las bloqueadas por menos, luego alfabético. Es el orden que libera el grafo más rápido.
def debt_key(n):
b = blockers(n)
return (len(b), nodes[n]["cls"], n)
return (len(b), -unblocks(n), nodes[n]["cls"], n)
debt_sorted = sorted(debt, key=debt_key)
ready = [n for n in debt_sorted if not blockers(n)]
@@ -92,13 +94,16 @@ for c in sorted(classes, key=lambda c: -sum(classes[c].values())):
# ---- filas de deuda (agrupadas visualmente por "listas" vs "bloqueadas") ----
def debt_row(n):
r = nodes[n]; b = blockers(n)
r = nodes[n]; b = blockers(n); u = unblocks(n)
deps = r.get("deps", [])
chips = "".join(chip(d) for d in deps) if deps else '<span class="chip empty">sin deps de build</span>'
badge = ('<span class="ready">lista</span>' if not b
else f'<span class="blocked">espera {len(b)}</span>')
if not b:
impact = f' <span class="impact" title="desbloquea {u} recetas en deuda">↑{u}</span>' if u else ""
badge = f'<span class="ready">lista</span>{impact}'
else:
badge = f'<span class="blocked">espera {len(b)}</span>'
ver = f'<span class="ver">{E(str(r.get("version") or ""))}</span>' if r.get("version") else ""
return f'''<tr class="drow" data-cls="{r['cls']}" data-ready="{'1' if not b else '0'}" data-name="{E(n)}">
return f'''<tr class="drow" data-cls="{r['cls']}" data-ready="{'1' if not b else '0'}" data-name="{E(n)}" data-unblocks="{u}">
<td class="c-name"><span class="s-dot s-{r['state']}"></span>{E(n)} {ver}</td>
<td class="c-cls"><span class="tag t-{r['cls']}">{E(CLASS_ES.get(r['cls'], r['cls']))}</span></td>
<td class="c-badge">{badge}</td>
@@ -205,6 +210,8 @@ td{padding:10px 14px;border-bottom:1px solid var(--line);vertical-align:top}
.t-rust{background:#dd6b3d22;color:var(--ember)} .t-gui{background:#8f623422;color:var(--never)}
.t-kernel{background:#bb443c22;color:var(--unhashable)}
.ready{color:var(--sealed);font-weight:600;font-size:.8rem}
.impact{display:inline-block;background:var(--ember);color:#fff;font-family:ui-monospace,monospace;
font-size:.72rem;font-weight:650;padding:1px 6px;border-radius:999px;margin-left:4px}
.blocked{color:var(--debt);font-weight:600;font-size:.8rem;font-family:ui-monospace,monospace}
.c-deps{max-width:560px}
.chip{display:inline-block;font-family:ui-monospace,monospace;font-size:.74rem;padding:2px 7px;
@@ -281,8 +288,9 @@ HTML = f'''<style>{CSS}</style>
<p class="eyebrow">huecos, en orden de ataque</p>
<p class="hint">Cada receta en deuda con sus deps de build coloreadas por estado. Una fila
<b style="color:var(--sealed)">lista</b> tiene todas sus deps al día ⇒ se puede construir ya;
una <b style="color:var(--debt)">espera N</b> depende de N recetas que también están en deuda.
{len(ready)} listas ahora.</p>
el pastilla <span class="impact">↑N</span> es cuántas otras en deuda desbloquea (ordenadas por
ahí: construí primero las de más impacto). Una <b style="color:var(--debt)">espera N</b> depende
de N recetas que también están en deuda. {len(ready)} listas ahora.</p>
<div class="controls">
{cls_btns}
<button class="fbtn" id="ready" data-ready aria-pressed="false">sólo listas</button>
+12
View File
@@ -126,6 +126,18 @@ def main():
order = toposort(recs)
# Lectura ACCIONABLE del grafo topológico: para cada receta, qué deps la BLOQUEAN (están en
# deuda) y a cuántas otras en deuda DESBLOQUEA (la tienen como dep). Una receta en deuda sin
# bloqueadores es construible YA; ordenar las listas por `unblocks` da el orden que libera más.
DEBT_STATES = ("debt", "never", "unhashable")
for n, r in recs.items():
r["blocked_by"] = [d for d in r.get("deps", [])
if d in recs and recs[d]["state"] in DEBT_STATES]
for n, r in recs.items():
# cuántas recetas EN DEUDA me declaran como dep directa (mi impacto de desbloqueo).
r["unblocks"] = sum(1 for m, rm in recs.items()
if rm["state"] in DEBT_STATES and n in rm.get("deps", []))
from collections import Counter
by_state = Counter(r["state"] for r in recs.values())
by_cls = Counter(r["cls"] for r in recs.values() if "cls" in r)