#!/usr/bin/env python3 # build-state-view.py — vista HUMANA del grafo de estado. Lee docs/state/build-state.json (la fuente # firme que emite build-state.py) y emite docs/state/build-state.html: un dashboard self-contained # (sin recursos externos) para VER y SEGUIR el avance, los huecos y los fallos. # # El dato clave que añade sobre el JSON crudo: para cada receta en deuda, computa quién la BLOQUEA # —sus deps que también están en deuda— usando el grafo. Una receta en deuda con TODAS sus deps # selladas está "lista para construir ya"; una con deps en deuda debe esperar. Ese es el uso # accionable del orden topológico: qué atacar primero. # # Uso: scripts/build-state-view.py # regenera docs/state/build-state.html desde el JSON import json, html from pathlib import Path from datetime import datetime, timezone ROOT = Path(__file__).resolve().parent.parent DATA = ROOT / "docs/state/build-state.json" OUT = ROOT / "docs/state/build-state.html" doc = json.loads(DATA.read_text()) nodes = doc["nodes"] STATE_ORDER = ["sealed", "debt", "never", "unhashable"] 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"} # blocked_by / unblocks los computa build-state.py sobre el grafo; acá sólo los leemos. def blockers(name): 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 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), -unblocks(n), nodes[n]["cls"], n) debt_sorted = sorted(debt, key=debt_key) ready = [n for n in debt_sorted if not blockers(n)] totals = doc["totals"] n_total = totals["recipes"] # Reparto por clase (para las barras por-clase). classes = {} for n, r in nodes.items(): c = r.get("cls", "c") classes.setdefault(c, {s: 0 for s in STATE_ORDER}) classes[c][r["state"]] += 1 def pct(x): return f"{100*x/n_total:.1f}%" E = html.escape def chip(dep): st = nodes[dep]["state"] if dep in nodes else "unhashable" return f'{E(dep)}' # ---- barra de progreso apilada (sealed|debt|never|unhashable) ---- seg = "" for s in STATE_ORDER: v = totals.get(s, 0) if v: seg += f'' # ---- tiles de estado ---- tiles = "" for s in STATE_ORDER: v = totals.get(s, 0) tiles += f'''
{v}
{E(STATE_ES[s])}
{pct(v)}
''' # ---- grid por clase ---- cls_cards = "" for c in sorted(classes, key=lambda c: -sum(classes[c].values())): cc = classes[c]; tot = sum(cc.values()) bar = "" for s in STATE_ORDER: if cc[s]: bar += f'' deuda = cc["debt"] + cc["never"] + cc["unhashable"] cls_cards += f'''
{E(CLASS_ES.get(c, c))} {tot}
{bar}
{cc['sealed']} al día {deuda} en deuda
''' # ---- filas de deuda (agrupadas visualmente por "listas" vs "bloqueadas") ---- def debt_row(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 'sin deps de build' if not b: impact = f' ↑{u}' if u else "" badge = f'lista{impact}' else: badge = f'espera {len(b)}' ver = f'{E(str(r.get("version") or ""))}' if r.get("version") else "" return f''' {E(n)} {ver} {E(CLASS_ES.get(r['cls'], r['cls']))} {badge} {chips} ''' rows = "".join(debt_row(n) for n in debt_sorted) gen_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") topo = "cierra sin ciclos" if doc.get("topo_ok") else "⚠ CICLO detectado" orphan_note = ("grafo cerrado" if not doc.get("orphan_deps") else f"⚠ {len(doc['orphan_deps'])} deps huérfanas") CSS = """ :root{ --bg:#eef0f3; --panel:#ffffff; --panel-2:#f6f7f9; --ink:#1b1e24; --ink-2:#5b636f; --line:#dce0e6; --steel:#4a7291; --ember:#dd6b3d; --sealed:#3f8f61; --debt:#bf922f; --never:#8f6234; --unhashable:#bb443c; --sealed-bg:#e7f1eb; --debt-bg:#f6efdc; --never-bg:#efe6da; --unhashable-bg:#f4e2e0; } @media (prefers-color-scheme:dark){:root{ --bg:#12151a; --panel:#1a1e25; --panel-2:#20252e; --ink:#e7eaef; --ink-2:#98a1af; --line:#2a303a; --steel:#7aa6c8; --ember:#e97d4f; --sealed:#59b483; --debt:#d8b054; --never:#b98a54; --unhashable:#d76258; --sealed-bg:#182a20; --debt-bg:#2a2617; --never-bg:#271f16; --unhashable-bg:#2c1a19; }} :root[data-theme="light"]{ --bg:#eef0f3; --panel:#ffffff; --panel-2:#f6f7f9; --ink:#1b1e24; --ink-2:#5b636f; --line:#dce0e6; --steel:#4a7291; --ember:#dd6b3d; --sealed:#3f8f61; --debt:#bf922f; --never:#8f6234; --unhashable:#bb443c; --sealed-bg:#e7f1eb; --debt-bg:#f6efdc; --never-bg:#efe6da; --unhashable-bg:#f4e2e0; } :root[data-theme="dark"]{ --bg:#12151a; --panel:#1a1e25; --panel-2:#20252e; --ink:#e7eaef; --ink-2:#98a1af; --line:#2a303a; --steel:#7aa6c8; --ember:#e97d4f; --sealed:#59b483; --debt:#d8b054; --never:#b98a54; --unhashable:#d76258; --sealed-bg:#182a20; --debt-bg:#2a2617; --never-bg:#271f16; --unhashable-bg:#2c1a19; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink); font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;line-height:1.5; -webkit-font-smoothing:antialiased} .mono{font-family:ui-monospace,"SF Mono","JetBrains Mono","Cascadia Code",Menlo,monospace} .wrap{max-width:1180px;margin:0 auto;padding:clamp(20px,4vw,48px)} header{display:flex;flex-wrap:wrap;align-items:baseline;gap:12px 20px;margin-bottom:28px} h1{font-size:clamp(1.5rem,3vw,2.1rem);margin:0;font-weight:680;letter-spacing:-.02em} h1 .mark{color:var(--ember)} .sub{color:var(--ink-2);font-size:.9rem} .sub .mono{color:var(--ink)} .eyebrow{text-transform:uppercase;letter-spacing:.12em;font-size:.7rem;font-weight:650; color:var(--ink-2);margin:0 0 10px} section{margin-bottom:34px} /* progress */ .progress{height:34px;display:flex;border-radius:8px;overflow:hidden;border:1px solid var(--line); background:var(--panel)} .seg{display:block;height:100%} .s-sealed{background:var(--sealed)} .s-debt{background:var(--debt)} .s-never{background:var(--never)} .s-unhashable{background:var(--unhashable)} .legend{display:flex;flex-wrap:wrap;gap:16px;margin-top:12px;font-size:.82rem;color:var(--ink-2)} .legend b{color:var(--ink);font-weight:600} .legend .k{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:6px; vertical-align:baseline} /* tiles */ .tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:14px} @media(max-width:640px){.tiles{grid-template-columns:repeat(2,1fr)}} .tile{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px 18px; border-top:3px solid var(--line)} .tile.s-sealed{border-top-color:var(--sealed)} .tile.s-debt{border-top-color:var(--debt)} .tile.s-never{border-top-color:var(--never)} .tile.s-unhashable{border-top-color:var(--unhashable)} .tile-n{font-size:2rem;font-weight:680;font-family:ui-monospace,monospace;line-height:1; font-variant-numeric:tabular-nums} .tile-l{font-size:.82rem;color:var(--ink-2);margin-top:6px} .tile-p{font-size:.78rem;color:var(--ink-2);margin-top:2px;font-family:ui-monospace,monospace} /* class grid */ .cls-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px} .cls{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:15px 16px} .cls-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px} .cls-name{font-weight:620} .cls-tot{font-family:ui-monospace,monospace;color:var(--ink-2); font-variant-numeric:tabular-nums} .bar{height:9px;display:flex;border-radius:5px;overflow:hidden;background:var(--panel-2)} .cls-foot{display:flex;justify-content:space-between;margin-top:9px;font-size:.78rem} .cls-foot .ok{color:var(--sealed)} .cls-foot .warn{color:var(--debt)} .cls-foot .muted{color:var(--ink-2)} /* debt table */ .controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:14px} .controls input{background:var(--panel);border:1px solid var(--line);color:var(--ink); padding:8px 12px;border-radius:8px;font-size:.9rem;min-width:200px;font-family:ui-monospace,monospace} .controls input:focus{outline:2px solid var(--steel);outline-offset:1px} .fbtn{background:var(--panel);border:1px solid var(--line);color:var(--ink-2);padding:7px 13px; border-radius:999px;font-size:.8rem;cursor:pointer;font-weight:550} .fbtn[aria-pressed="true"]{background:var(--steel);border-color:var(--steel);color:#fff} .tbl-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:10px;background:var(--panel)} table{border-collapse:collapse;width:100%;font-size:.88rem} th{text-align:left;font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-2); font-weight:600;padding:11px 14px;border-bottom:1px solid var(--line);background:var(--panel-2); position:sticky;top:0} td{padding:10px 14px;border-bottom:1px solid var(--line);vertical-align:top} .drow:last-child td{border-bottom:none} .c-name{font-family:ui-monospace,monospace;font-weight:550;white-space:nowrap} .c-name .ver{color:var(--ink-2);font-weight:400;font-size:.82em;margin-left:5px} .s-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:8px; vertical-align:middle} .tag{font-size:.72rem;padding:2px 8px;border-radius:999px;font-weight:600;white-space:nowrap} .t-c{background:var(--panel-2);color:var(--ink-2)} .t-go{background:#1f3a4d33;color:var(--steel)} .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; border-radius:5px;margin:2px 3px 2px 0;border:1px solid transparent} .chip.s-sealed{background:var(--sealed-bg);color:var(--sealed)} .chip.s-debt{background:var(--debt-bg);color:var(--debt);border-color:var(--debt)} .chip.s-never{background:var(--never-bg);color:var(--never);border-color:var(--never)} .chip.s-unhashable{background:var(--unhashable-bg);color:var(--unhashable);border-color:var(--unhashable)} .chip.empty{color:var(--ink-2);font-style:italic} .hint{color:var(--ink-2);font-size:.82rem;margin:0 0 14px} .count{color:var(--ink-2);font-size:.82rem;margin-left:auto;font-family:ui-monospace,monospace} footer{color:var(--ink-2);font-size:.78rem;margin-top:36px;padding-top:16px; border-top:1px solid var(--line);line-height:1.7} footer .mono{color:var(--ink)} @media(prefers-reduced-motion:no-preference){.seg,.fbtn{transition:.15s}} """ JS = """ const rows=[...document.querySelectorAll('.drow')]; const q=document.getElementById('q'); let cls='all',onlyReady=false; function apply(){ const t=(q.value||'').toLowerCase();let shown=0; for(const r of rows){ const okC=cls==='all'||r.dataset.cls===cls; const okR=!onlyReady||r.dataset.ready==='1'; const okT=!t||r.dataset.name.includes(t); const v=okC&&okR&&okT;r.style.display=v?'':'none';if(v)shown++; } document.getElementById('count').textContent=shown+' en deuda'; } document.querySelectorAll('.fbtn[data-cls]').forEach(b=>b.onclick=()=>{ document.querySelectorAll('.fbtn[data-cls]').forEach(x=>x.setAttribute('aria-pressed','false')); b.setAttribute('aria-pressed','true');cls=b.dataset.cls;apply();}); const rb=document.getElementById('ready'); rb.onclick=()=>{onlyReady=!onlyReady;rb.setAttribute('aria-pressed',onlyReady);apply();}; q.oninput=apply; """ cls_btns = '' for c in sorted(classes, key=lambda c: -sum(classes[c].values())): deuda = classes[c]["debt"] + classes[c]["never"] + classes[c]["unhashable"] if deuda: cls_btns += f'' HTML = f'''

estado del build · hammer

{n_total} recetas · grafo {E(topo)} · {E(orphan_note)} · {E(gen_time)}

avance del corpus

{seg}
{totals.get('sealed',0)} sellado al día {totals.get('debt',0)} deuda de rebuild {totals.get('never',0)} nunca construida {''+str(totals.get('unhashable',0))+' ilegible' if totals.get('unhashable') else ''}
{tiles}

dónde está la deuda

{cls_cards}

huecos, en orden de ataque

Cada receta en deuda con sus deps de build coloreadas por estado. Una fila lista tiene todas sus deps al día ⇒ se puede construir ya; el pastilla ↑N es cuántas otras en deuda desbloquea (ordenadas por ahí: construí primero las de más impacto). Una espera N depende de N recetas que también están en deuda. {len(ready)} listas ahora.

{cls_btns} {len(debt_sorted)} en deuda
{rows}
recetaclaseestadodeps de build
''' OUT.write_text(HTML) print(f"→ {OUT.relative_to(ROOT)} ({len(HTML)//1024} KB) · {len(debt_sorted)} en deuda, {len(ready)} listas")