Files
takana/scripts/triaje.py
T
sergioandClaude Opus 4.8 ec28a9b394 catálogo objetivo P5: tandas a archivo, y el triaje que cierra el lazo
tandas/README.md declara los 55 ficheros ARCHIVO de campañas pasadas (no se
borran: su git log es la crónica de cada frente) y documenta de dónde sale el
trabajo ahora. import-batch.sh -f sigue vivo como vía de escape, no como camino.

Lo que faltaba para que la cola fuera derivada de verdad era el paso humano, y
estaba sin forma: 137 candidatos clasificables sólo en la cabeza de alguien.
triaje.py les da forma durable — docs/state/frontera-triaje.toml, 170 candidatos
unificados de los 3 perfiles. Cada veredicto cierra un lazo distinto:

  hueco    → raíz en targets.toml ⇒ nace un nodo `wanted`, el sembrador lo
             siembra y el drenaje lo ordena (= trabajo nuevo para el worker)
  opcional → registrado, deja de reaparecer como pregunta
  nix-ismo → vuelve a seed-graph.py como descarte ⇒ el sembrador APRENDE de su
             propio triaje y cada corrida sale más limpia

Sincronizar NUNCA pisa un juicio humano: agrega los nuevos y marca ausente=true
los que dejaron de aparecer (progreso, o cambio de nixpkgs — se ve en el diff).
`sugerencia` propone con evidencia mecánica pero `veredicto` arranca en
`pendiente`: la heurística abarata el juicio, no lo cierra.

Lazo verificado punta a punta: python3-3.14.6-env marcado nix-ismo → --aplicar
lo escribe al descarte → seed-graph.normalizar() lo devuelve clasificado y deja
de contarlo como hueco. Probado y revertido; los 170 siguen pendientes.

Cadena cerrada y corriendo: targets.toml → build-state.json → drenaje.json →
worker, regenerada por el latido cada 30min. El único paso que pide una persona
es el triaje.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:20:30 -04:00

197 lines
9.0 KiB
Python
Executable File
Raw 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.
#
# 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"
VEREDICTOS = ("pendiente", "hueco", "opcional", "nix-ismo")
# 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",
"# 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.",
"#",
"# `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("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
sugerencia=sug, porque=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)")
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())