250 líneas de comentario en 151 scripts. Control verificado: el diff no toca NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa. El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces: las tres eran el MOTD que el script escribe DENTRO de la imagen construida — texto del producto, no comentario del script. Se cambiaron aparte y a propósito, que es rebranding, no limpieza. Y el hallazgo caro: casaba contra , que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate. La etapa 4 lo movió a y el script quedó casando NADA. No fallaba: imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja. Además 14 rutas de módulo en docs, que el barrido anterior no tocó porque no es frontera de palabra.
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 takana 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 takana 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())
|