Files
Sergio 476168bb07 takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
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.
2026-09-09 19:28:48 +00: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 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())