gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 604Rust 56Shell 36Markdown 281227 Definiciones

⎇ 7db765a311cb06d9650b4eddbdcb9030301f9402 scripts/build-state-view.py Python · 344 líneas · 17.6 KB Historia Quién tocó qué crudo

el visor del grafo reventaba con un estado que no conocía 5d16b55e1 · Sergio ·

1#!/usr/bin/env python3
2# build-state-view.py — vista HUMANA del grafo de estado. Lee docs/state/build-state.json (la fuente
3# firme que emite build-state.py) y emite docs/state/build-state.html: un dashboard self-contained
4# (sin recursos externos) para VER y SEGUIR el avance, los huecos y los fallos.
5#
6# El dato clave que añade sobre el JSON crudo: para cada receta en deuda, computa quién la BLOQUEA
7# —sus deps que también están en deuda— usando el grafo. Una receta en deuda con TODAS sus deps
8# selladas está "lista para construir ya"; una con deps en deuda debe esperar. Ese es el uso
9# accionable del orden topológico: qué atacar primero.
10#
11# Uso:   scripts/build-state-view.py         # corpus → docs/state/build-state.html
12#        scripts/build-state-view.py --kde   # frente KDE → docs/state/build-state-kde.html
13import json, html, sys
14from pathlib import Path
15from datetime import datetime, timezone
16
17ROOT = Path(__file__).resolve().parent.parent
18KDE = "--kde" in sys.argv
19DATA = ROOT / ("docs/state/build-state-kde.json" if KDE else "docs/state/build-state.json")
20OUT = ROOT / ("docs/state/build-state-kde.html" if KDE else "docs/state/build-state.html")
21
22doc = json.loads(DATA.read_text())
23nodes = doc["nodes"]
24
25# Los estados que SÍ son del corpus de recetas: son los que entran a la barra y a los tiles, y su
26# suma es `totals["recipes"]`.
27STATE_ORDER = ["sealed", "debt", "never", "unhashable"]
28STATE_ES = {"sealed": "sellado al día", "debt": "deuda de rebuild",
29            "never": "nunca construida", "unhashable": "ilegible"}
30# Y los que NO lo son: `wanted` es un hueco declarado en targets.toml (no hay receta todavía) y
31# `ajeno` es una imagen de qorpa (ADR 0015), que no se construye desde fuente. `build-state.py` los
32# excluye de `totals["recipes"]` a propósito, así que meterlos en la barra la haría pasar del 100 %.
33# Antes ni siquiera se los nombraba: el visor reventaba con KeyError al toparse con uno, o sea que
34# **agregar un estado nuevo rompía el tablero entero**. Ahora se cuentan aparte y se muestran.
35STATE_FUERA = {"wanted": "querida (sin receta aún)", "ajeno": "imagen ajena (qorpa)"}
36CLASS_ES = {"c": "C", "go": "Go", "rust": "Rust", "gui": "GUI", "kernel": "kernel"}
37
38# blocked_by / unblocks los computa build-state.py sobre el grafo; acá sólo los leemos.
39def blockers(name):
40    return nodes[name].get("blocked_by", [])
41def unblocks(name):
42    return nodes[name].get("unblocks", 0)
43
44debt = {n: r for n, r in nodes.items() if r["state"] in ("debt", "never", "unhashable")}
45# Orden de ATAQUE: primero las "listas" (sin bloqueadores), y entre ellas la que DESBLOQUEA más
46# arriba; luego las bloqueadas por menos, luego alfabético. Es el orden que libera el grafo más rápido.
47def debt_key(n):
48    b = blockers(n)
49    return (len(b), -unblocks(n), nodes[n]["cls"], n)
50debt_sorted = sorted(debt, key=debt_key)
51ready = [n for n in debt_sorted if not blockers(n)]
52
53totals = doc["totals"]
54n_total = totals["recipes"]
55
56# Reparto por clase (para las barras por-clase).
57classes = {}
58fuera = {}
59for n, r in nodes.items():
60    st = r["state"]
61    if st not in STATE_ORDER:      # wanted / ajeno / cualquiera que aparezca mañana
62        fuera[st] = fuera.get(st, 0) + 1
63        continue
64    c = r.get("cls", "c")
65    classes.setdefault(c, {s: 0 for s in STATE_ORDER})
66    classes[c][st] += 1
67
68def pct(x): return f"{100*x/n_total:.1f}%"
69
70E = html.escape
71
72def chip(dep):
73    st = nodes[dep]["state"] if dep in nodes else "unhashable"
74    return f'<span class="chip s-{st}" title="{E(STATE_ES.get(st, st))}">{E(dep)}</span>'
75
76# ---- barra de progreso apilada (sealed|debt|never|unhashable) ----
77seg = ""
78for s in STATE_ORDER:
79    v = totals.get(s, 0)
80    if v:
81        seg += f'<span class="seg s-{s}" style="width:{100*v/n_total}%" title="{E(STATE_ES[s])}: {v}"></span>'
82
83# ---- tiles de estado ----
84tiles = ""
85for s in STATE_ORDER:
86    v = totals.get(s, 0)
87    tiles += f'''<div class="tile s-{s}">
88      <div class="tile-n">{v}</div>
89      <div class="tile-l">{E(STATE_ES[s])}</div>
90      <div class="tile-p">{pct(v)}</div>
91    </div>'''
92
93# ---- grid por clase ----
94cls_cards = ""
95for c in sorted(classes, key=lambda c: -sum(classes[c].values())):
96    cc = classes[c]; tot = sum(cc.values())
97    bar = ""
98    for s in STATE_ORDER:
99        if cc[s]:
100            bar += f'<span class="seg s-{s}" style="width:{100*cc[s]/tot}%"></span>'
101    deuda = cc["debt"] + cc["never"] + cc["unhashable"]
102    cls_cards += f'''<div class="cls">
103      <div class="cls-head"><span class="cls-name">{E(CLASS_ES.get(c, c))}</span>
104        <span class="cls-tot">{tot}</span></div>
105      <div class="bar">{bar}</div>
106      <div class="cls-foot"><span class="ok">{cc['sealed']} al día</span>
107        <span class="{'warn' if deuda else 'muted'}">{deuda} en deuda</span></div>
108    </div>'''
109
110# ---- filas de deuda (agrupadas visualmente por "listas" vs "bloqueadas") ----
111def debt_row(n):
112    r = nodes[n]; b = blockers(n); u = unblocks(n)
113    deps = r.get("deps", [])
114    chips = "".join(chip(d) for d in deps) if deps else '<span class="chip empty">sin deps de build</span>'
115    if not b:
116        impact = f' <span class="impact" title="desbloquea {u} recetas en deuda">↑{u}</span>' if u else ""
117        badge = f'<span class="ready">lista</span>{impact}'
118    else:
119        badge = f'<span class="blocked">espera {len(b)}</span>'
120    ver = f'<span class="ver">{E(str(r.get("version") or ""))}</span>' if r.get("version") else ""
121    return f'''<tr class="drow" data-cls="{r['cls']}" data-ready="{'1' if not b else '0'}" data-name="{E(n)}" data-unblocks="{u}">
122      <td class="c-name"><span class="s-dot s-{r['state']}"></span>{E(n)} {ver}</td>
123      <td class="c-cls"><span class="tag t-{r['cls']}">{E(CLASS_ES.get(r['cls'], r['cls']))}</span></td>
124      <td class="c-badge">{badge}</td>
125      <td class="c-deps">{chips}</td>
126    </tr>'''
127
128rows = "".join(debt_row(n) for n in debt_sorted)
129gen_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
130topo = "cierra sin ciclos" if doc.get("topo_ok") else "⚠ CICLO detectado"
131orphan_note = ("grafo cerrado" if not doc.get("orphan_deps")
132               else f"⚠ {len(doc['orphan_deps'])} deps huérfanas")
133
134CSS = """
135:root{
136  --bg:#eef0f3; --panel:#ffffff; --panel-2:#f6f7f9; --ink:#1b1e24; --ink-2:#5b636f;
137  --line:#dce0e6; --steel:#4a7291; --ember:#dd6b3d;
138  --sealed:#3f8f61; --debt:#bf922f; --never:#8f6234; --unhashable:#bb443c;
139  --sealed-bg:#e7f1eb; --debt-bg:#f6efdc; --never-bg:#efe6da; --unhashable-bg:#f4e2e0;
140}
141@media (prefers-color-scheme:dark){:root{
142  --bg:#12151a; --panel:#1a1e25; --panel-2:#20252e; --ink:#e7eaef; --ink-2:#98a1af;
143  --line:#2a303a; --steel:#7aa6c8; --ember:#e97d4f;
144  --sealed:#59b483; --debt:#d8b054; --never:#b98a54; --unhashable:#d76258;
145  --sealed-bg:#182a20; --debt-bg:#2a2617; --never-bg:#271f16; --unhashable-bg:#2c1a19;
146}}
147:root[data-theme="light"]{
148  --bg:#eef0f3; --panel:#ffffff; --panel-2:#f6f7f9; --ink:#1b1e24; --ink-2:#5b636f;
149  --line:#dce0e6; --steel:#4a7291; --ember:#dd6b3d;
150  --sealed:#3f8f61; --debt:#bf922f; --never:#8f6234; --unhashable:#bb443c;
151  --sealed-bg:#e7f1eb; --debt-bg:#f6efdc; --never-bg:#efe6da; --unhashable-bg:#f4e2e0;
152}
153:root[data-theme="dark"]{
154  --bg:#12151a; --panel:#1a1e25; --panel-2:#20252e; --ink:#e7eaef; --ink-2:#98a1af;
155  --line:#2a303a; --steel:#7aa6c8; --ember:#e97d4f;
156  --sealed:#59b483; --debt:#d8b054; --never:#b98a54; --unhashable:#d76258;
157  --sealed-bg:#182a20; --debt-bg:#2a2617; --never-bg:#271f16; --unhashable-bg:#2c1a19;
158}
159*{box-sizing:border-box}
160body{margin:0;background:var(--bg);color:var(--ink);
161  font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;line-height:1.5;
162  -webkit-font-smoothing:antialiased}
163.mono{font-family:ui-monospace,"SF Mono","JetBrains Mono","Cascadia Code",Menlo,monospace}
164.wrap{max-width:1180px;margin:0 auto;padding:clamp(20px,4vw,48px)}
165header{display:flex;flex-wrap:wrap;align-items:baseline;gap:12px 20px;margin-bottom:28px}
166h1{font-size:clamp(1.5rem,3vw,2.1rem);margin:0;font-weight:680;letter-spacing:-.02em}
167h1 .mark{color:var(--ember)}
168.sub{color:var(--ink-2);font-size:.9rem}
169.sub .mono{color:var(--ink)}
170.eyebrow{text-transform:uppercase;letter-spacing:.12em;font-size:.7rem;font-weight:650;
171  color:var(--ink-2);margin:0 0 10px}
172section{margin-bottom:34px}
173/* progress */
174.progress{height:34px;display:flex;border-radius:8px;overflow:hidden;border:1px solid var(--line);
175  background:var(--panel)}
176.seg{display:block;height:100%}
177.s-sealed{background:var(--sealed)} .s-debt{background:var(--debt)}
178.s-never{background:var(--never)} .s-unhashable{background:var(--unhashable)}
179.legend{display:flex;flex-wrap:wrap;gap:16px;margin-top:12px;font-size:.82rem;color:var(--ink-2)}
180.legend b{color:var(--ink);font-weight:600}
181.legend .k{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:6px;
182  vertical-align:baseline}
183/* tiles */
184.tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
185@media(max-width:640px){.tiles{grid-template-columns:repeat(2,1fr)}}
186.tile{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px 18px;
187  border-top:3px solid var(--line)}
188.tile.s-sealed{border-top-color:var(--sealed)} .tile.s-debt{border-top-color:var(--debt)}
189.tile.s-never{border-top-color:var(--never)} .tile.s-unhashable{border-top-color:var(--unhashable)}
190.tile-n{font-size:2rem;font-weight:680;font-family:ui-monospace,monospace;line-height:1;
191  font-variant-numeric:tabular-nums}
192.tile-l{font-size:.82rem;color:var(--ink-2);margin-top:6px}
193.tile-p{font-size:.78rem;color:var(--ink-2);margin-top:2px;font-family:ui-monospace,monospace}
194/* class grid */
195.cls-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px}
196.cls{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:15px 16px}
197.cls-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px}
198.cls-name{font-weight:620} .cls-tot{font-family:ui-monospace,monospace;color:var(--ink-2);
199  font-variant-numeric:tabular-nums}
200.bar{height:9px;display:flex;border-radius:5px;overflow:hidden;background:var(--panel-2)}
201.cls-foot{display:flex;justify-content:space-between;margin-top:9px;font-size:.78rem}
202.cls-foot .ok{color:var(--sealed)} .cls-foot .warn{color:var(--debt)}
203.cls-foot .muted{color:var(--ink-2)}
204/* debt table */
205.controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:14px}
206.controls input{background:var(--panel);border:1px solid var(--line);color:var(--ink);
207  padding:8px 12px;border-radius:8px;font-size:.9rem;min-width:200px;font-family:ui-monospace,monospace}
208.controls input:focus{outline:2px solid var(--steel);outline-offset:1px}
209.fbtn{background:var(--panel);border:1px solid var(--line);color:var(--ink-2);padding:7px 13px;
210  border-radius:999px;font-size:.8rem;cursor:pointer;font-weight:550}
211.fbtn[aria-pressed="true"]{background:var(--steel);border-color:var(--steel);color:#fff}
212.tbl-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:10px;background:var(--panel)}
213table{border-collapse:collapse;width:100%;font-size:.88rem}
214th{text-align:left;font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-2);
215  font-weight:600;padding:11px 14px;border-bottom:1px solid var(--line);background:var(--panel-2);
216  position:sticky;top:0}
217td{padding:10px 14px;border-bottom:1px solid var(--line);vertical-align:top}
218.drow:last-child td{border-bottom:none}
219.c-name{font-family:ui-monospace,monospace;font-weight:550;white-space:nowrap}
220.c-name .ver{color:var(--ink-2);font-weight:400;font-size:.82em;margin-left:5px}
221.s-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:8px;
222  vertical-align:middle}
223.tag{font-size:.72rem;padding:2px 8px;border-radius:999px;font-weight:600;white-space:nowrap}
224.t-c{background:var(--panel-2);color:var(--ink-2)} .t-go{background:#1f3a4d33;color:var(--steel)}
225.t-rust{background:#dd6b3d22;color:var(--ember)} .t-gui{background:#8f623422;color:var(--never)}
226.t-kernel{background:#bb443c22;color:var(--unhashable)}
227.ready{color:var(--sealed);font-weight:600;font-size:.8rem}
228.impact{display:inline-block;background:var(--ember);color:#fff;font-family:ui-monospace,monospace;
229  font-size:.72rem;font-weight:650;padding:1px 6px;border-radius:999px;margin-left:4px}
230.blocked{color:var(--debt);font-weight:600;font-size:.8rem;font-family:ui-monospace,monospace}
231.c-deps{max-width:560px}
232.chip{display:inline-block;font-family:ui-monospace,monospace;font-size:.74rem;padding:2px 7px;
233  border-radius:5px;margin:2px 3px 2px 0;border:1px solid transparent}
234.chip.s-sealed{background:var(--sealed-bg);color:var(--sealed)}
235.chip.s-debt{background:var(--debt-bg);color:var(--debt);border-color:var(--debt)}
236.chip.s-never{background:var(--never-bg);color:var(--never);border-color:var(--never)}
237.chip.s-unhashable{background:var(--unhashable-bg);color:var(--unhashable);border-color:var(--unhashable)}
238.chip.s-wanted,.chip.s-ajeno{background:var(--panel);color:var(--ink-2);border-color:var(--line)}
239.chip.empty{color:var(--ink-2);font-style:italic}
240.hint{color:var(--ink-2);font-size:.82rem;margin:0 0 14px}
241.count{color:var(--ink-2);font-size:.82rem;margin-left:auto;font-family:ui-monospace,monospace}
242footer{color:var(--ink-2);font-size:.78rem;margin-top:36px;padding-top:16px;
243  border-top:1px solid var(--line);line-height:1.7}
244footer .mono{color:var(--ink)}
245@media(prefers-reduced-motion:no-preference){.seg,.fbtn{transition:.15s}}
246"""
247
248JS = """
249const rows=[...document.querySelectorAll('.drow')];
250const q=document.getElementById('q');
251let cls='all',onlyReady=false;
252function apply(){
253  const t=(q.value||'').toLowerCase();let shown=0;
254  for(const r of rows){
255    const okC=cls==='all'||r.dataset.cls===cls;
256    const okR=!onlyReady||r.dataset.ready==='1';
257    const okT=!t||r.dataset.name.includes(t);
258    const v=okC&&okR&&okT;r.style.display=v?'':'none';if(v)shown++;
259  }
260  document.getElementById('count').textContent=shown+' en deuda';
261}
262document.querySelectorAll('.fbtn[data-cls]').forEach(b=>b.onclick=()=>{
263  document.querySelectorAll('.fbtn[data-cls]').forEach(x=>x.setAttribute('aria-pressed','false'));
264  b.setAttribute('aria-pressed','true');cls=b.dataset.cls;apply();});
265const rb=document.getElementById('ready');
266rb.onclick=()=>{onlyReady=!onlyReady;rb.setAttribute('aria-pressed',onlyReady);apply();};
267q.oninput=apply;
268"""
269
270cls_btns = '<button class="fbtn" data-cls="all" aria-pressed="true">todas</button>'
271for c in sorted(classes, key=lambda c: -sum(classes[c].values())):
272    deuda = classes[c]["debt"] + classes[c]["never"] + classes[c]["unhashable"]
273    if deuda:
274        cls_btns += f'<button class="fbtn" data-cls="{c}" aria-pressed="false">{E(CLASS_ES.get(c,c))} ({deuda})</button>'
275
276scope = "frente KDE" if KDE else "hammer"
277by_queue = doc.get("by_queue", {})
278queue_line = ""
279if "incoming-kde" in by_queue and len(by_queue) > 1:
280    kq = by_queue["incoming-kde"]; kd = kq.get("debt", 0) + kq.get("never", 0)
281    queue_line = (f' · <b>cola KDE {kq.get("sealed",0)}/{sum(kq.values())} selladas</b>, '
282                  f'{kd} en deuda')
283
284HTML = f'''<style>{CSS}</style>
285<div class="wrap">
286  <header>
287    <h1>estado del build <span class="mark">·</span> <span class="mono">{E(scope)}</span></h1>
288    <div class="sub">{n_total} recetas{queue_line} · grafo {E(topo)} · {E(orphan_note)} · <span class="mono">{E(gen_time)}</span></div>
289  </header>
290
291  <section>
292    <p class="eyebrow">avance del corpus</p>
293    <div class="progress">{seg}</div>
294    <div class="legend">
295      <span><span class="k s-sealed"></span><b>{totals.get('sealed',0)}</b> sellado al día</span>
296      <span><span class="k s-debt"></span><b>{totals.get('debt',0)}</b> deuda de rebuild</span>
297      <span><span class="k s-never"></span><b>{totals.get('never',0)}</b> nunca construida</span>
298      {''.join(f'<span><b>{v}</b> {E(STATE_FUERA.get(k, k))} (fuera del corpus)</span>' for k, v in sorted(fuera.items()))}
299      {'<span><span class="k s-unhashable"></span><b>'+str(totals.get('unhashable',0))+'</b> ilegible</span>' if totals.get('unhashable') else ''}
300    </div>
301  </section>
302
303  <section>
304    <div class="tiles">{tiles}</div>
305  </section>
306
307  <section>
308    <p class="eyebrow">dónde está la deuda</p>
309    <div class="cls-grid">{cls_cards}</div>
310  </section>
311
312  <section>
313    <p class="eyebrow">huecos, en orden de ataque</p>
314    <p class="hint">Cada receta en deuda con sus deps de build coloreadas por estado. Una fila
315      <b style="color:var(--sealed)">lista</b> tiene todas sus deps al día ⇒ se puede construir ya;
316      el pastilla <span class="impact">↑N</span> es cuántas otras en deuda desbloquea (ordenadas por
317      ahí: construí primero las de más impacto). Una <b style="color:var(--debt)">espera N</b> depende
318      de N recetas que también están en deuda. {len(ready)} listas ahora.</p>
319    <div class="controls">
320      {cls_btns}
321      <button class="fbtn" id="ready" data-ready aria-pressed="false">sólo listas</button>
322      <input id="q" placeholder="buscar receta…" spellcheck="false" aria-label="buscar receta">
323      <span class="count" id="count">{len(debt_sorted)} en deuda</span>
324    </div>
325    <div class="tbl-wrap">
326      <table>
327        <thead><tr><th>receta</th><th>clase</th><th>estado</th><th>deps de build</th></tr></thead>
328        <tbody>{rows}</tbody>
329      </table>
330    </div>
331  </section>
332
333  <footer>
334    Derivado de <span class="mono">recipes/*.toml</span> + <span class="mono">hammer hash</span> + el store,
335    por <span class="mono">scripts/build-state.py</span>. La fuente firme es
336    <span class="mono">docs/state/build-state.json</span> (versionada: su <span class="mono">git diff</span> es el avance).
337    Regenerar: <span class="mono">scripts/build-state.py && scripts/build-state-view.py</span>.
338  </footer>
339</div>
340<script>{JS}</script>
341'''
342
343OUT.write_text(HTML)
344print(f"→ {OUT.relative_to(ROOT)} ({len(HTML)//1024} KB) · {len(debt_sorted)} en deuda, {len(ready)} listas")