Evidencia dura primero: NINGUNO de los 170 bloquea un build. Toda receta que
pide uno de estos ya está sellada al menos una vez ⇒ hammer construye sin
ninguno. Eso descarta empíricamente "hueco de build" para los 170 y deja sólo
la pregunta de runtime.
VEREDICTOS
provisto 11 — no faltan: ya los da otra receta con otro nombre. nixpkgs parte
en varios paquetes lo que acá es uno solo. Verificado contra el
store: wayland-scanner→wayland, mesa-libgbm→mesa (nuestros tres
mesa producen libgbm.so.1), libxcb-*→xcb-util-*, poppler-qt6→
poppler, gmp-with-cxx→gmp, uname→coreutils.
nix-ismo 5 — andamiaje de nixpkgs (env wrappers, helpers del stdenv, glibc
asomando por su libc).
opcional 150 — software real que nixpkgs habilita y hammer no necesita, con el
porqué agrupado: systemd (usamos arje-zero), X11 heredado (el
escritorio es Wayland), Vulkan/shaders, audio/multimedia,
conectores de BD, tooling de docs/tests, bindings Python de Qt,
paquetería ajena (tenemos .swm).
hueco 4 — gaps de RUNTIME, no de build: shared-mime-info (sin base MIME
no hay tipos de fichero), xwayland (ninguna app X11 corre),
polkit-qt-1 (sin diálogos de autorización), qqc2-breeze-style
(los controles QML caen a un estilo genérico).
CORRIGE UN ERROR MÍO DE P3: dije que mesa-libgbm/libglvnd eran "el muro de
GBM/EGL". Falso — libgbm ya lo produce nuestro mesa. El muro era softpipe vs
llvmpipe, no un paquete ausente.
CUARTO VEREDICTO NUEVO (`provisto`) con su propio lazo: sale a alias-triaje.txt
y seed-graph.py lo carga en MAPA ⇒ esos 11 nombres dejan de contarse como hueco
para siempre. Junto con nixismos-triaje.txt, el sembrador aprende de su triaje.
Y LA CADENA SE EJERCE ENTERA POR PRIMERA VEZ: los 4 huecos entraron como raíces
de escritorio-kde → nacieron 4 nodos `wanted` (raíces 11, sin receta 4; el grafo
sigue cerrando, --check exit 0) → seed-graph los sembró (42 aristas conocidas,
21 candidatos nuevos de frontera) → drenar.py los ordena marcándolos [semilla],
que es la regla de la fuente única a la vista. qqc2-breeze-style no cae en la
onda 1 porque sus deps SEMBRADAS la traban (kcodecs, kirigami…): el andamio
funcionando como se diseñó.
De paso: qtbase se selló mientras corría esto ⇒ la onda 1 de KDE se abrió de 1 a
13 recetas. El cuello de botella que reportó P4 ya está destrabado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
215 lines
10 KiB
Python
Executable File
215 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
# triaje.py — convierte los CANDIDATOS de la frontera en VEREDICTOS durables. (P5 del plan
|
||
# docs/plan-catalogo-objetivo.md.)
|
||
#
|
||
# POR QUÉ. `seed-graph.py --frontera` produce hipótesis: 137 nombres que nixpkgs pide y hammer no
|
||
# puede construir. Clasificarlos es decisión HUMANA (la regla de la granja: el worker MIDE, el hub
|
||
# CLASIFICA). Pero si ese juicio se queda en la cabeza o en un mensaje, se repite entero cada vez que
|
||
# alguien vuelve a correr el sembrador. Este fichero lo hace durable y consumible por máquina.
|
||
#
|
||
# TRES VEREDICTOS, tres destinos distintos — y ahí está el valor, porque cada uno CIERRA un lazo:
|
||
# hueco → falta de verdad ⇒ va como raíz a `targets.toml` y nace un nodo `wanted` en el grafo,
|
||
# que el sembrador siembra y el drenaje ordena. Es lo que da trabajo nuevo al worker.
|
||
# opcional → dep que nixpkgs habilita y hammer no quiere (gstreamer en un escritorio sin audio).
|
||
# Queda registrada para que deje de reaparecer como pregunta.
|
||
# nix-ismo → andamiaje de nixpkgs sin contraparte acá ⇒ vuelve a `seed-graph.py` como descarte,
|
||
# así la próxima corrida es más limpia. El sembrador aprende de su propio triaje.
|
||
# provisto → NO falta: ya lo da otra receta con otro nombre (nixpkgs parte en varios paquetes
|
||
# lo que acá es uno solo: `mesa-libgbm`→mesa, `libxcb-image`→xcb-util-image).
|
||
# Vuelve a `seed-graph.py` como ALIAS ⇒ deja de contarse como hueco para siempre.
|
||
#
|
||
# NO decide nada solo. `sugerencia` propone con evidencia mecánica (cuántas recetas lo piden, si el
|
||
# nombre tiene forma de env/wrapper de nix), pero `veredicto` arranca en `pendiente` y sólo lo
|
||
# cambia un humano editando el TOML.
|
||
#
|
||
# Uso: scripts/triaje.py # sincroniza candidatos → docs/state/frontera-triaje.toml
|
||
# scripts/triaje.py --resumen # cuántos pendientes/clasificados hay
|
||
# scripts/triaje.py --aplicar # veredictos → descarte del sembrador + líneas para targets.toml
|
||
import json, re, sys, tomllib
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
FRONTERA = ROOT / "docs/state/seed-frontera.json"
|
||
TRIAJE = ROOT / "docs/state/frontera-triaje.toml"
|
||
DESCARTE = ROOT / "docs/state/nixismos-triaje.txt"
|
||
ALIAS = ROOT / "docs/state/alias-triaje.txt"
|
||
VEREDICTOS = ("pendiente", "hueco", "opcional", "nix-ismo", "provisto")
|
||
|
||
# Formas de nombre que en nixpkgs son andamiaje casi seguro. Sólo alimenta `sugerencia`, nunca
|
||
# `veredicto`: una heurística no cierra un juicio, lo abarata.
|
||
FORMA_NIXISMO = re.compile(r"(-env$|^python3-[\d.]+|-minimal(-libs)?$|^glibc|-wrapper$|\.sh$)")
|
||
|
||
|
||
def cargar_triaje():
|
||
if not TRIAJE.exists():
|
||
return {}
|
||
d = tomllib.load(open(TRIAJE, "rb"))
|
||
return d.get("candidato", {})
|
||
|
||
|
||
def sugerir(dep, piden):
|
||
if FORMA_NIXISMO.search(dep):
|
||
return "nix-ismo?", "el nombre tiene forma de andamiaje de nixpkgs (env/minimal/wrapper)"
|
||
if len(piden) >= 4:
|
||
return "hueco?", f"lo piden {len(piden)} recetas de la imagen — difícil que sea accesorio"
|
||
if len(piden) == 1:
|
||
return "opcional?", f"lo pide una sola receta ({piden[0]}); mirá si es una feature opcional"
|
||
return "", f"lo piden {len(piden)} recetas"
|
||
|
||
|
||
def escribir(cands):
|
||
lineas = [
|
||
"# frontera-triaje.toml — VEREDICTOS sobre los candidatos a hueco de la frontera.",
|
||
"#",
|
||
"# Lo genera y sincroniza `scripts/triaje.py` desde `seed-frontera.json`; los veredictos los",
|
||
"# pone un HUMANO editando este fichero. Sincronizar NO pisa lo ya decidido: sólo agrega",
|
||
"# candidatos nuevos y marca `[ausente]` los que dejaron de aparecer.",
|
||
"#",
|
||
"# veredicto ∈ pendiente | hueco | opcional | nix-ismo | provisto",
|
||
"# hueco — falta de verdad. Ponelo como raíz en docs/state/targets.toml ⇒ nace un nodo",
|
||
"# `wanted` en el grafo y entra al drenaje. (`triaje.py --aplicar` te da la línea.)",
|
||
"# opcional — dep que nixpkgs habilita y nosotros no queremos. Queda registrada para que no",
|
||
"# vuelva a preguntarse.",
|
||
"# nix-ismo — andamiaje de nixpkgs sin contraparte acá. `--aplicar` lo manda al descarte del",
|
||
"# sembrador ⇒ la próxima corrida sale más limpia.",
|
||
"# provisto — no falta: ya lo da otra receta con otro nombre. `provee = \"<receta>\"` dice cuál;",
|
||
"# `--aplicar` lo manda al sembrador como alias.",
|
||
"#",
|
||
"# `sugerencia` es una heurística con evidencia, NO un veredicto: abarata el juicio, no lo cierra.",
|
||
'schema = "hammer-frontera-triaje/1"',
|
||
"",
|
||
]
|
||
for dep in sorted(cands):
|
||
c = cands[dep]
|
||
lineas.append(f'[candidato."{dep}"]')
|
||
lineas.append(f'veredicto = "{c.get("veredicto", "pendiente")}"')
|
||
if c.get("sugerencia"):
|
||
lineas.append(f'sugerencia = "{c["sugerencia"]}"')
|
||
if c.get("provee"):
|
||
lineas.append(f'provee = "{c["provee"]}"')
|
||
if c.get("porque"):
|
||
lineas.append(f'porque = "{c["porque"]}"')
|
||
lineas.append(f'perfiles = {json.dumps(sorted(c.get("perfiles", [])))}')
|
||
piden = sorted(c.get("piden", []))
|
||
lineas.append(f'piden = {json.dumps(piden[:12])}'
|
||
+ (f" # …y {len(piden)-12} más" if len(piden) > 12 else ""))
|
||
if c.get("ausente"):
|
||
lineas.append("ausente = true # ya no aparece en el último barrido de la frontera")
|
||
lineas.append("")
|
||
TRIAJE.write_text("\n".join(lineas))
|
||
|
||
|
||
def sincronizar():
|
||
if not FRONTERA.exists():
|
||
print(f"falta {FRONTERA.relative_to(ROOT)} — corré scripts/seed-graph.py --frontera <perfil>",
|
||
file=sys.stderr)
|
||
return 1
|
||
fr = json.loads(FRONTERA.read_text())
|
||
previo = cargar_triaje()
|
||
|
||
vistos = {}
|
||
for perfil, d in (fr.get("perfiles") or {}).items():
|
||
for dep, quienes in (d.get("candidatos") or {}).items():
|
||
e = vistos.setdefault(dep, {"perfiles": set(), "piden": set()})
|
||
e["perfiles"].add(perfil)
|
||
e["piden"].update(quienes)
|
||
|
||
cands, nuevos = {}, 0
|
||
for dep, e in vistos.items():
|
||
sug, porque = sugerir(dep, sorted(e["piden"]))
|
||
anterior = previo.get(dep, {})
|
||
if not anterior:
|
||
nuevos += 1
|
||
cands[dep] = dict(
|
||
veredicto=anterior.get("veredicto", "pendiente"), # NUNCA se pisa un juicio humano
|
||
provee=anterior.get("provee"), sugerencia=sug,
|
||
porque=anterior.get("porque") or porque,
|
||
perfiles=e["perfiles"], piden=e["piden"])
|
||
# Los que ya no aparecen no se borran: se marcan. Un candidato que desaparece puede ser progreso
|
||
# (se escribió la receta) o un cambio de nixpkgs, y conviene verlo en el `git diff`.
|
||
ausentes = 0
|
||
for dep, c in previo.items():
|
||
if dep not in cands:
|
||
cands[dep] = dict(c, ausente=True,
|
||
perfiles=set(c.get("perfiles", [])), piden=set(c.get("piden", [])))
|
||
ausentes += 1
|
||
|
||
escribir(cands)
|
||
print(f"== triaje sincronizado → {TRIAJE.relative_to(ROOT)}")
|
||
print(f" {len(cands)} candidatos ({nuevos} nuevos, {ausentes} ya ausentes)")
|
||
return resumen()
|
||
|
||
|
||
def resumen():
|
||
cands = cargar_triaje()
|
||
if not cands:
|
||
print(" (sin triaje todavía)")
|
||
return 0
|
||
cuenta = {}
|
||
for c in cands.values():
|
||
cuenta[c.get("veredicto", "pendiente")] = cuenta.get(c.get("veredicto", "pendiente"), 0) + 1
|
||
print("\n== veredictos")
|
||
for v in VEREDICTOS:
|
||
if cuenta.get(v):
|
||
print(f" {v:10} {cuenta[v]:4}")
|
||
pend = [d for d, c in cands.items() if c.get("veredicto", "pendiente") == "pendiente"]
|
||
if pend:
|
||
print(f"\n== los pendientes que MÁS recetas piden (empezá por acá)")
|
||
for d in sorted(pend, key=lambda d: -len(cands[d].get("piden", [])))[:12]:
|
||
c = cands[d]
|
||
s = f" [{c['sugerencia']}]" if c.get("sugerencia") else ""
|
||
print(f" {len(c.get('piden', [])):3}× {d:28}{s}")
|
||
return 0
|
||
|
||
|
||
def aplicar():
|
||
cands = cargar_triaje()
|
||
if not cands:
|
||
print("no hay triaje que aplicar (corré scripts/triaje.py primero)", file=sys.stderr)
|
||
return 1
|
||
nixismos = sorted(d for d, c in cands.items() if c.get("veredicto") == "nix-ismo")
|
||
DESCARTE.write_text(
|
||
"# nixismos-triaje.txt — DERIVADO de frontera-triaje.toml por scripts/triaje.py --aplicar.\n"
|
||
"# Lo lee seed-graph.py y lo suma a su lista de descarte: el sembrador aprende de su triaje.\n"
|
||
+ "\n".join(nixismos) + ("\n" if nixismos else ""))
|
||
print(f"== {len(nixismos)} nix-ismos → {DESCARTE.relative_to(ROOT)} (los lee seed-graph.py)")
|
||
|
||
alias = {d: c["provee"] for d, c in cands.items()
|
||
if c.get("veredicto") == "provisto" and c.get("provee")}
|
||
ALIAS.write_text(
|
||
"# alias-triaje.txt — DERIVADO de frontera-triaje.toml por scripts/triaje.py --aplicar.\n"
|
||
"# `<nombre-en-nixpkgs> <receta-de-hammer-que-lo-provee>`. Lo lee seed-graph.py como MAPA:\n"
|
||
"# nixpkgs parte en varios paquetes lo que acá es uno solo.\n"
|
||
+ "".join(f"{d} {r}\n" for d, r in sorted(alias.items())))
|
||
print(f"== {len(alias)} alias → {ALIAS.relative_to(ROOT)} (los lee seed-graph.py como MAPA)")
|
||
|
||
huecos = {d: c for d, c in cands.items() if c.get("veredicto") == "hueco"}
|
||
if not huecos:
|
||
print("\n== 0 huecos confirmados: no hay raíces nuevas que agregar a targets.toml")
|
||
return 0
|
||
print(f"\n== {len(huecos)} huecos confirmados — pegá estas raíces en docs/state/targets.toml")
|
||
por_perfil = {}
|
||
for d, c in huecos.items():
|
||
for p in c.get("perfiles", []):
|
||
por_perfil.setdefault(p, []).append(d)
|
||
for p, ds in sorted(por_perfil.items()):
|
||
print(f"\n [perfil.{p}] — agregar a `paquetes`:")
|
||
print(" " + ", ".join(f'"{d}"' for d in sorted(ds)))
|
||
print("\n Después: scripts/build-state.py (nacen los nodos `wanted`)")
|
||
print(" scripts/seed-graph.py --perfil <p> (siembra sus aristas)")
|
||
print(" scripts/drenar.py --todos (entran al orden de masticado)")
|
||
return 0
|
||
|
||
|
||
def main():
|
||
args = sys.argv[1:]
|
||
if "--resumen" in args:
|
||
return resumen()
|
||
if "--aplicar" in args:
|
||
return aplicar()
|
||
return sincronizar()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|