Files
sergioandClaude Opus 4.8 9b67268a65 triaje: los 170 candidatos de frontera clasificados, y la cadena se ejerce entera
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>
2026-07-22 18:30:19 -04:00

215 lines
10 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())