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>
This commit is contained in:
@@ -241,6 +241,36 @@ alimenta de ahí en vez de listas escritas a mano.
|
|||||||
**Eso es lo que de verdad mata la sensación de caos: la cola del worker deja de ser autoría y pasa a
|
**Eso es lo que de verdad mata la sensación de caos: la cola del worker deja de ser autoría y pasa a
|
||||||
ser derivada.**
|
ser derivada.**
|
||||||
|
|
||||||
|
#### P5 — RESULTADO (2026-07-22)
|
||||||
|
|
||||||
|
`tandas/README.md` declara el directorio **archivo de campañas pasadas** (55 ficheros, no se borran:
|
||||||
|
su `git log` es la crónica de cada frente) y documenta de dónde sale el trabajo ahora. La vía de
|
||||||
|
escape (`import-batch.sh -f`) sigue viva; deja de ser el camino principal.
|
||||||
|
|
||||||
|
Lo que faltaba para que la cola fuera de verdad derivada era el paso humano, y estaba sin forma:
|
||||||
|
137 candidatos de frontera clasificables sólo en la cabeza de alguien. `scripts/triaje.py` le da
|
||||||
|
forma durable — `docs/state/frontera-triaje.toml`, **170 candidatos** unificados de los tres
|
||||||
|
perfiles. Cada veredicto CIERRA un lazo distinto, que es lo que lo hace algo más que una lista:
|
||||||
|
|
||||||
|
| veredicto | destino | efecto |
|
||||||
|
|---|---|---|
|
||||||
|
| `hueco` | raíz en `targets.toml` | nace un nodo `wanted` ⇒ el sembrador lo siembra y el drenaje lo ordena |
|
||||||
|
| `opcional` | queda registrado | deja de reaparecer como pregunta |
|
||||||
|
| `nix-ismo` | `nixismos-triaje.txt` | vuelve a `seed-graph.py` como descarte ⇒ **el sembrador aprende de su propio triaje** |
|
||||||
|
|
||||||
|
Sincronizar **nunca pisa un juicio humano**: agrega candidatos nuevos y marca `ausente = true` los
|
||||||
|
que dejaron de aparecer (que puede ser progreso —se escribió la receta— o un cambio de nixpkgs, y
|
||||||
|
conviene verlo en el `git diff`). El campo `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`: la heurística **abarata** el juicio, no lo cierra.
|
||||||
|
|
||||||
|
Lazo verificado de punta a punta: marcar `python3-3.14.6-env` como `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 en `pendiente` — clasificar es decisión del humano.)
|
||||||
|
|
||||||
|
**Estado de la cadena**: cerrada y corriendo. `targets.toml` → `build-state.json` → `drenaje.json` →
|
||||||
|
worker, regenerada por el latido cada 30 min. El único paso que requiere una persona es el triaje.
|
||||||
|
|
||||||
## 5. Secuencia (commits chicos, valor en el primero)
|
## 5. Secuencia (commits chicos, valor en el primero)
|
||||||
|
|
||||||
| paso | qué | riesgo | esfuerzo |
|
| paso | qué | riesgo | esfuerzo |
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
# nixismos-triaje.txt — DERIVADO de frontera-triaje.toml por scripts/triaje.py --aplicar.
|
||||||
|
# Lo lee seed-graph.py y lo suma a su lista de descarte: el sembrador aprende de su triaje.
|
||||||
@@ -69,6 +69,13 @@ NIXISMOS_EXACTOS = {
|
|||||||
"glibc-locales", "cmake-minimal",
|
"glibc-locales", "cmake-minimal",
|
||||||
}
|
}
|
||||||
NIXISMOS_SUFIJOS = ("-hook", "Hook", "-wrapper", "-setup-hook", ".sh")
|
NIXISMOS_SUFIJOS = ("-hook", "Hook", "-wrapper", "-setup-hook", ".sh")
|
||||||
|
# Lazo cerrado: los candidatos que un humano marcó `nix-ismo` en el triaje (P5) vuelven acá como
|
||||||
|
# descarte, así cada corrida del sembrador sale más limpia que la anterior. Si el fichero no está,
|
||||||
|
# no pasa nada — el sembrador funciona igual, sólo con más ruido.
|
||||||
|
_TRIAJE = ROOT / "docs/state/nixismos-triaje.txt"
|
||||||
|
if _TRIAJE.exists():
|
||||||
|
NIXISMOS_EXACTOS |= {l.strip() for l in _TRIAJE.read_text().splitlines()
|
||||||
|
if l.strip() and not l.startswith("#")}
|
||||||
NIXISMOS_INFIJOS = ("-hook",) # `cargo-build-hook.sh` no termina en `-hook`, lo contiene
|
NIXISMOS_INFIJOS = ("-hook",) # `cargo-build-hook.sh` no termina en `-hook`, lo contiene
|
||||||
# Toolchains que provee el LAB (el rootfs del sandbox), no una receta: ninguna receta Rust declara
|
# Toolchains que provee el LAB (el rootfs del sandbox), no una receta: ninguna receta Rust declara
|
||||||
# `rust` ni `cargo` en `[deps].build` — están vacías y el toolchain viene de abajo. Que nixpkgs los
|
# `rust` ni `cargo` en `[deps].build` — están vacías y el toolchain viene de abajo. Que nixpkgs los
|
||||||
|
|||||||
Executable
+196
@@ -0,0 +1,196 @@
|
|||||||
|
#!/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())
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# tandas/ — ARCHIVO de campañas pasadas
|
||||||
|
|
||||||
|
**Estos 55 ficheros son historia, no la cola de trabajo.** Se conservan porque documentan qué se
|
||||||
|
importó y por qué en cada campaña (el `git log` de cada uno es la crónica del frente Go, del
|
||||||
|
base-system, del stack GUI). No se borran. Pero **el trabajo nuevo ya no sale de acá**.
|
||||||
|
|
||||||
|
## Por qué dejaron de ser la cola
|
||||||
|
|
||||||
|
Una tanda era una lista plana escrita a mano. Eso tenía tres problemas que no se veían mientras
|
||||||
|
funcionaba:
|
||||||
|
|
||||||
|
1. **No decía el orden.** 40 nombres sin aristas: cuál primero, qué destraba qué, qué se puede
|
||||||
|
paralelizar — todo quedaba en la cabeza del que construía.
|
||||||
|
2. **No decía cuánto faltaba.** Una tanda terminada no significaba una imagen completa; nadie sabía
|
||||||
|
la distancia al objetivo porque el objetivo no estaba escrito en ningún lado.
|
||||||
|
3. **Había que autorarla.** El worker quedaba idle esperando que un humano escribiera la próxima
|
||||||
|
lista — el problema que [[vps-nunca-idle]] intentaba parchear a mano.
|
||||||
|
|
||||||
|
## De dónde sale el trabajo ahora
|
||||||
|
|
||||||
|
Todo se deriva del grafo de estado. El flujo completo, y qué comando corre cada paso:
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/state/targets.toml ← QUÉ debe tener la distro (perfiles = imágenes, sólo RAÍCES)
|
||||||
|
│ scripts/build-state.py
|
||||||
|
▼
|
||||||
|
docs/state/build-state.json ← QUÉ hay, qué falta (`wanted`), qué depende de qué
|
||||||
|
│ scripts/drenar.py --todos
|
||||||
|
▼
|
||||||
|
docs/state/drenaje.json ← EN QUÉ ORDEN: ondas topológicas por imagen
|
||||||
|
│
|
||||||
|
▼ el worker muele la onda 1: scripts/farm/campana-deuda.sh <recetas>
|
||||||
|
```
|
||||||
|
|
||||||
|
Y para descubrir lo que todavía no está declarado:
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts/seed-graph.py --frontera <perfil> → docs/state/seed-frontera.json (candidatos a hueco)
|
||||||
|
scripts/triaje.py → docs/state/frontera-triaje.toml (veredicto humano)
|
||||||
|
scripts/triaje.py --aplicar → raíces nuevas para targets.toml + descarte del sembrador
|
||||||
|
```
|
||||||
|
|
||||||
|
El triaje es el único paso que **requiere un humano**: decidir si un candidato es hueco real, dep
|
||||||
|
opcional que no queremos, o andamiaje de nixpkgs. Todo lo demás se deriva.
|
||||||
|
|
||||||
|
## Qué sigue vivo acá
|
||||||
|
|
||||||
|
- `import-batch.sh -f tandas/<x>.txt` sigue funcionando para importar una lista puntual a mano. Es
|
||||||
|
la vía de escape, no el camino principal.
|
||||||
|
- Los subdirectorios `needs-review-*` y `staged-fallidos*` son **resultados**, no colas: recetas que
|
||||||
|
fallaron un smoke-test y esperan diagnóstico.
|
||||||
Reference in New Issue
Block a user