#!/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 = \"\"` 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 ", 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" "# ` `. 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

(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())