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