gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ 466972e3010bda77ec242cf40c53abc43c948992 scripts/triaje.py Python · 214 líneas · 10.3 KB Historia Quién tocó qué crudo

takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4 476168bb0 · Sergio ·

1#!/usr/bin/env python3
2# triaje.py — convierte los CANDIDATOS de la frontera en VEREDICTOS durables. (P5 del plan
3# docs/plan-catalogo-objetivo.md.)
4#
5# POR QUÉ. `seed-graph.py --frontera` produce hipótesis: 137 nombres que nixpkgs pide y takana no
6# puede construir. Clasificarlos es decisión HUMANA (la regla de la granja: el worker MIDE, el hub
7# CLASIFICA). Pero si ese juicio se queda en la cabeza o en un mensaje, se repite entero cada vez que
8# alguien vuelve a correr el sembrador. Este fichero lo hace durable y consumible por máquina.
9#
10# TRES VEREDICTOS, tres destinos distintos — y ahí está el valor, porque cada uno CIERRA un lazo:
11#   hueco     → falta de verdad ⇒ va como raíz a `targets.toml` y nace un nodo `wanted` en el grafo,
12#               que el sembrador siembra y el drenaje ordena. Es lo que da trabajo nuevo al worker.
13#   opcional  → dep que nixpkgs habilita y takana no quiere (gstreamer en un escritorio sin audio).
14#               Queda registrada para que deje de reaparecer como pregunta.
15#   nix-ismo  → andamiaje de nixpkgs sin contraparte acá ⇒ vuelve a `seed-graph.py` como descarte,
16#               así la próxima corrida es más limpia. El sembrador aprende de su propio triaje.
17#   provisto  → NO falta: ya lo da otra receta con otro nombre (nixpkgs parte en varios paquetes
18#               lo que acá es uno solo: `mesa-libgbm`→mesa, `libxcb-image`→xcb-util-image).
19#               Vuelve a `seed-graph.py` como ALIAS ⇒ deja de contarse como hueco para siempre.
20#
21# NO decide nada solo. `sugerencia` propone con evidencia mecánica (cuántas recetas lo piden, si el
22# nombre tiene forma de env/wrapper de nix), pero `veredicto` arranca en `pendiente` y sólo lo
23# cambia un humano editando el TOML.
24#
25# Uso:  scripts/triaje.py              # sincroniza candidatos → docs/state/frontera-triaje.toml
26#       scripts/triaje.py --resumen    # cuántos pendientes/clasificados hay
27#       scripts/triaje.py --aplicar    # veredictos → descarte del sembrador + líneas para targets.toml
28import json, re, sys, tomllib
29from pathlib import Path
30
31ROOT = Path(__file__).resolve().parent.parent
32FRONTERA = ROOT / "docs/state/seed-frontera.json"
33TRIAJE = ROOT / "docs/state/frontera-triaje.toml"
34DESCARTE = ROOT / "docs/state/nixismos-triaje.txt"
35ALIAS = ROOT / "docs/state/alias-triaje.txt"
36VEREDICTOS = ("pendiente", "hueco", "opcional", "nix-ismo", "provisto")
37
38# Formas de nombre que en nixpkgs son andamiaje casi seguro. Sólo alimenta `sugerencia`, nunca
39# `veredicto`: una heurística no cierra un juicio, lo abarata.
40FORMA_NIXISMO = re.compile(r"(-env$|^python3-[\d.]+|-minimal(-libs)?$|^glibc|-wrapper$|\.sh$)")
41
42
43def cargar_triaje():
44    if not TRIAJE.exists():
45        return {}
46    d = tomllib.load(open(TRIAJE, "rb"))
47    return d.get("candidato", {})
48
49
50def sugerir(dep, piden):
51    if FORMA_NIXISMO.search(dep):
52        return "nix-ismo?", "el nombre tiene forma de andamiaje de nixpkgs (env/minimal/wrapper)"
53    if len(piden) >= 4:
54        return "hueco?", f"lo piden {len(piden)} recetas de la imagen — difícil que sea accesorio"
55    if len(piden) == 1:
56        return "opcional?", f"lo pide una sola receta ({piden[0]}); mirá si es una feature opcional"
57    return "", f"lo piden {len(piden)} recetas"
58
59
60def escribir(cands):
61    lineas = [
62        "# frontera-triaje.toml — VEREDICTOS sobre los candidatos a hueco de la frontera.",
63        "#",
64        "# Lo genera y sincroniza `scripts/triaje.py` desde `seed-frontera.json`; los veredictos los",
65        "# pone un HUMANO editando este fichero. Sincronizar NO pisa lo ya decidido: sólo agrega",
66        "# candidatos nuevos y marca `[ausente]` los que dejaron de aparecer.",
67        "#",
68        "# veredicto ∈ pendiente | hueco | opcional | nix-ismo | provisto",
69        "#   hueco    — falta de verdad. Ponelo como raíz en docs/state/targets.toml ⇒ nace un nodo",
70        "#              `wanted` en el grafo y entra al drenaje. (`triaje.py --aplicar` te da la línea.)",
71        "#   opcional — dep que nixpkgs habilita y nosotros no queremos. Queda registrada para que no",
72        "#              vuelva a preguntarse.",
73        "#   nix-ismo — andamiaje de nixpkgs sin contraparte acá. `--aplicar` lo manda al descarte del",
74        "#              sembrador ⇒ la próxima corrida sale más limpia.",
75        "#   provisto — no falta: ya lo da otra receta con otro nombre. `provee = \"<receta>\"` dice cuál;",
76        "#              `--aplicar` lo manda al sembrador como alias.",
77        "#",
78        "# `sugerencia` es una heurística con evidencia, NO un veredicto: abarata el juicio, no lo cierra.",
79        'schema = "hammer-frontera-triaje/1"',
80        "",
81    ]
82    for dep in sorted(cands):
83        c = cands[dep]
84        lineas.append(f'[candidato."{dep}"]')
85        lineas.append(f'veredicto = "{c.get("veredicto", "pendiente")}"')
86        if c.get("sugerencia"):
87            lineas.append(f'sugerencia = "{c["sugerencia"]}"')
88        if c.get("provee"):
89            lineas.append(f'provee = "{c["provee"]}"')
90        if c.get("porque"):
91            lineas.append(f'porque = "{c["porque"]}"')
92        lineas.append(f'perfiles = {json.dumps(sorted(c.get("perfiles", [])))}')
93        piden = sorted(c.get("piden", []))
94        lineas.append(f'piden = {json.dumps(piden[:12])}'
95                      + (f"  # …y {len(piden)-12} más" if len(piden) > 12 else ""))
96        if c.get("ausente"):
97            lineas.append("ausente = true  # ya no aparece en el último barrido de la frontera")
98        lineas.append("")
99    TRIAJE.write_text("\n".join(lineas))
100
101
102def sincronizar():
103    if not FRONTERA.exists():
104        print(f"falta {FRONTERA.relative_to(ROOT)} — corré scripts/seed-graph.py --frontera <perfil>",
105              file=sys.stderr)
106        return 1
107    fr = json.loads(FRONTERA.read_text())
108    previo = cargar_triaje()
109
110    vistos = {}
111    for perfil, d in (fr.get("perfiles") or {}).items():
112        for dep, quienes in (d.get("candidatos") or {}).items():
113            e = vistos.setdefault(dep, {"perfiles": set(), "piden": set()})
114            e["perfiles"].add(perfil)
115            e["piden"].update(quienes)
116
117    cands, nuevos = {}, 0
118    for dep, e in vistos.items():
119        sug, porque = sugerir(dep, sorted(e["piden"]))
120        anterior = previo.get(dep, {})
121        if not anterior:
122            nuevos += 1
123        cands[dep] = dict(
124            veredicto=anterior.get("veredicto", "pendiente"),  # NUNCA se pisa un juicio humano
125            provee=anterior.get("provee"), sugerencia=sug,
126            porque=anterior.get("porque") or porque,
127            perfiles=e["perfiles"], piden=e["piden"])
128    # Los que ya no aparecen no se borran: se marcan. Un candidato que desaparece puede ser progreso
129    # (se escribió la receta) o un cambio de nixpkgs, y conviene verlo en el `git diff`.
130    ausentes = 0
131    for dep, c in previo.items():
132        if dep not in cands:
133            cands[dep] = dict(c, ausente=True,
134                              perfiles=set(c.get("perfiles", [])), piden=set(c.get("piden", [])))
135            ausentes += 1
136
137    escribir(cands)
138    print(f"== triaje sincronizado → {TRIAJE.relative_to(ROOT)}")
139    print(f"   {len(cands)} candidatos ({nuevos} nuevos, {ausentes} ya ausentes)")
140    return resumen()
141
142
143def resumen():
144    cands = cargar_triaje()
145    if not cands:
146        print("   (sin triaje todavía)")
147        return 0
148    cuenta = {}
149    for c in cands.values():
150        cuenta[c.get("veredicto", "pendiente")] = cuenta.get(c.get("veredicto", "pendiente"), 0) + 1
151    print("\n== veredictos")
152    for v in VEREDICTOS:
153        if cuenta.get(v):
154            print(f"   {v:10} {cuenta[v]:4}")
155    pend = [d for d, c in cands.items() if c.get("veredicto", "pendiente") == "pendiente"]
156    if pend:
157        print(f"\n== los pendientes que MÁS recetas piden (empezá por acá)")
158        for d in sorted(pend, key=lambda d: -len(cands[d].get("piden", [])))[:12]:
159            c = cands[d]
160            s = f" [{c['sugerencia']}]" if c.get("sugerencia") else ""
161            print(f"   {len(c.get('piden', [])):3}×  {d:28}{s}")
162    return 0
163
164
165def aplicar():
166    cands = cargar_triaje()
167    if not cands:
168        print("no hay triaje que aplicar (corré scripts/triaje.py primero)", file=sys.stderr)
169        return 1
170    nixismos = sorted(d for d, c in cands.items() if c.get("veredicto") == "nix-ismo")
171    DESCARTE.write_text(
172        "# nixismos-triaje.txt — DERIVADO de frontera-triaje.toml por scripts/triaje.py --aplicar.\n"
173        "# Lo lee seed-graph.py y lo suma a su lista de descarte: el sembrador aprende de su triaje.\n"
174        + "\n".join(nixismos) + ("\n" if nixismos else ""))
175    print(f"== {len(nixismos)} nix-ismos → {DESCARTE.relative_to(ROOT)} (los lee seed-graph.py)")
176
177    alias = {d: c["provee"] for d, c in cands.items()
178             if c.get("veredicto") == "provisto" and c.get("provee")}
179    ALIAS.write_text(
180        "# alias-triaje.txt — DERIVADO de frontera-triaje.toml por scripts/triaje.py --aplicar.\n"
181        "# `<nombre-en-nixpkgs> <receta-de-hammer-que-lo-provee>`. Lo lee seed-graph.py como MAPA:\n"
182        "# nixpkgs parte en varios paquetes lo que acá es uno solo.\n"
183        + "".join(f"{d} {r}\n" for d, r in sorted(alias.items())))
184    print(f"== {len(alias)} alias → {ALIAS.relative_to(ROOT)} (los lee seed-graph.py como MAPA)")
185
186    huecos = {d: c for d, c in cands.items() if c.get("veredicto") == "hueco"}
187    if not huecos:
188        print("\n== 0 huecos confirmados: no hay raíces nuevas que agregar a targets.toml")
189        return 0
190    print(f"\n== {len(huecos)} huecos confirmados — pegá estas raíces en docs/state/targets.toml")
191    por_perfil = {}
192    for d, c in huecos.items():
193        for p in c.get("perfiles", []):
194            por_perfil.setdefault(p, []).append(d)
195    for p, ds in sorted(por_perfil.items()):
196        print(f"\n   [perfil.{p}] — agregar a `paquetes`:")
197        print("     " + ", ".join(f'"{d}"' for d in sorted(ds)))
198    print("\n   Después:  scripts/build-state.py  (nacen los nodos `wanted`)")
199    print("             scripts/seed-graph.py --perfil <p>   (siembra sus aristas)")
200    print("             scripts/drenar.py --todos            (entran al orden de masticado)")
201    return 0
202
203
204def main():
205    args = sys.argv[1:]
206    if "--resumen" in args:
207        return resumen()
208    if "--aplicar" in args:
209        return aplicar()
210    return sincronizar()
211
212
213if __name__ == "__main__":
214    sys.exit(main())