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 815Rust 56Shell 37Markdown 291229 Definiciones

yupana: modelo EXTRA-GRAFO — `preflight` aprende los patrones de hueco de entorno

El grafo modela deps (R1: Receta×Receta). El entorno es OTRA capa: R2 (Receta×
Capacidad, qué exige un build) y R3 (Host×Capacidad, qué ofrece la máquina).
Construible = clausura(R1) ∧ R2⊆R3. Esos huecos viven FUERA del grafo (infra del
sandbox, toolchain, layout de fs) y una matemática de grafos sola no los ve —
pero SÍ se aprenden, como patrones con predictor, calibrados contra fallos reales.

`yupana preflight <receta>` corre los patrones ANTES de construir. Primer patrón:
overlay-lowerdir, la cicatriz de kio. Habría marcado kio antes de quemar la
campaña descubriéndolo a los golpes:

  kio: 52 deps ⇒ lowerdir ~5346B > 4096B (una página) ⇒ depende del merge

CALIBRADO, no adivinado: el estimador daba corto (2974B "ok") hasta usar el
prefijo que bwrap VE en runtime (/oldroot/…, ruta del worker, no la del hub).
Con eso: kio estimado 5346B vs real 5315B — 31B de error, sin fudge. El primer
predictor "obvio" mentía con confianza; la lección de siempre.

preflight --perfil escritorio-kde destapa lo sistémico: las 40 recetas KDE
profundas CUELGAN del merge (plasma-desktop: 114 deps → 11708B, casi 3× el
límite). Todo el escritorio dependía del fix 6a0dc40. El pre-flight vuelve
VISIBLE y contable una fragilidad que antes sólo se manifestaba fallando.

Registro extensible (PATRONES_ENTORNO): futuros = capacidad-de-host (rootfs
laptop≠worker, techo MSRV — acumular observaciones), skew de toolchain (zig-skew
— fingerprint por artefacto). harkaq ya MIDE R2 (evidencia negativa); esto lo
vuelve consultable + pre-vuela. Guardián: kio marcado + estimador calibrado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

8c10c0a908b374f9ba574889b829cb56cfd84fa0 · por sergio · (hace 3 meses) · padre 7f2ebdbb8 · ver el árbol en este commit

Delta semántico · 5

scripts/test-yupana-radio.py

  • cambió por dentro fn main

scripts/yupana.py

2 archivos · +116 −0

diff --git a/scripts/test-yupana-radio.py b/scripts/test-yupana-radio.py
index 147b0a17..23f97274 100755
--- a/scripts/test-yupana-radio.py
+++ b/scripts/test-yupana-radio.py
@@ -64,6 +64,18 @@ def main():
             fallos.append("corpus/dbus e incoming-kde/dbus tienen build_fp IGUAL — el detector "
                           "los marcaría redundantes y son variantes estático/dinámico deliberadas")
 
+    # 4) MODELO EXTRA-GRAFO: el patrón de overlay debe seguir marcando kio (la cicatriz) y su
+    #    estimación seguir calibrada contra el fallo real (5315B). Si el estimador se descalibra
+    #    (p.ej. alguien cambia el prefijo del hub por el del sandbox), el pre-flight miente.
+    if ("incoming-kde", "kio") in {(q, n) for q in colas for n in colas[q]}:
+        deps_kio = colas["incoming-kde"]["kio"]
+        est = yupana._lowerdir_estimado(deps_kio)
+        if yupana.patron_overlay("kio", deps_kio) is None:
+            fallos.append("el patrón de overlay NO marca a kio — el pre-flight dejó de ver el hueco")
+        elif abs(est - 5315) > 600:  # el fallo real midió 5315B; margen por variación de nombres
+            fallos.append(f"el estimador de lowerdir se descalibró: kio={est}B vs real 5315B "
+                          f"(¿prefijo equivocado? el del hub da corto)")
+
     if fallos:
         print("✗ REGRESIÓN de yupana:")
         for f in fallos:
@@ -75,6 +87,8 @@ def main():
           f"{sorted(memb.get(('corpus','libdrm'), set()))}")
     print(f"✓ detector agrupa por fuente (itstool↔gettext-tiny) y no cría lobos "
           f"(itstool=carrier, dbus=variante)")
+    print(f"✓ modelo extra-grafo: preflight marca kio (lowerdir ~{yupana._lowerdir_estimado(colas['incoming-kde']['kio'])}B "
+          f"vs real 5315B, calibrado)")
     return 0
 
 if __name__ == "__main__":
diff --git a/scripts/yupana.py b/scripts/yupana.py
index 8a4de3a6..2f359389 100755
--- a/scripts/yupana.py
+++ b/scripts/yupana.py
@@ -35,6 +35,7 @@
 #   perfiles <receta> ¿qué imágenes dependen de esto? (nativo)
 #   duplicados        invariante (c): nudos que son el mismo paquete bajo dos nombres (por sha) (nativo)
 #   keystones         nudos en deuda que gatean el máximo trabajo bloqueado (cierre inverso, AND) (nativo)
+#   preflight <r>     modelo EXTRA-GRAFO: ¿choca con un límite del entorno que el grafo no ve? (nativo)
 #   estado            regenera el grafo firme          → build-state.py
 #   objetivo          qué debe tener cada imagen        → targets.py --lista
 #   frontera <perfil> qué pide upstream y no tenemos    → seed-graph.py --frontera
@@ -104,6 +105,104 @@ def _base(n):
 # ACCIONABLE es el cierre inverso restringido a la DEUDA — cuánto trabajo BLOQUEADO libero. El
 # dominador SÍ tiene un uso legítimo, otro: la RETENCIÓN (lo exclusivamente mío), abajo.
 
+# --- MODELO EXTRA-GRAFO: patrones de hueco de ENTORNO -------------------------------------------
+# El grafo modela deps (R1: Receta×Receta). El entorno es OTRA capa: R2 (Receta×Capacidad, qué exige
+# un build) y R3 (Host×Capacidad, qué ofrece la máquina). Construible = clausura(R1) ∧ R2⊆R3. Estos
+# huecos viven FUERA del grafo (infra del sandbox, toolchain, layout de fs) y una matemática de grafos
+# sola no los ve — pero SÍ se aprenden, como patrones con predictor, calibrados contra fallos REALES.
+# Cada patrón: (nombre, deps) → veredicto|None. `preflight` los corre ANTES de construir.
+#
+# HOY: 1 patrón (overlay-lowerdir), la cicatriz de kio (2026-07-22). Futuros: capacidad-de-host
+# (rootfs laptop≠worker, techo MSRV — acumular observaciones (receta,host,lo-que-faltó)); skew de
+# toolchain (zig-skew — fingerprint del toolchain por artefacto). harkaq ya MIDE R2 (evidencia
+# negativa: qué path del entorno tocó un build); esto lo vuelve consultable + pre-vuela.
+#
+# CALIBRADO, no adivinado: el estimador daba corto hasta usar el prefijo que bwrap VE (`/oldroot/...`,
+# la ruta runtime del worker, no la del hub). Con eso: kio estimado 5346B vs real 5315B (31B de error).
+# El primer predictor "obvio" (prefijo del hub) mentía con confianza — la lección de siempre.
+PAGINA_OVERLAY = 4096  # tope del string de opciones de un mount overlayfs = PAGE_SIZE
+# Prefijo que bwrap ve en runtime (bajo /oldroot, ruta del worker). Ajustable por host.
+_PREF_SANDBOX = os.environ.get("PREF_STORE_SANDBOX", "/oldroot/opt/hammer/store/")
+_OVERHEAD_OVERLAY = len("upperdir=/tmp-overlay-upper-0,workdir=/tmp-overlay-work-0,") \
+    + len("/oldroot/opt/hammer/.dev-fs/alpine") + len(",userxattr")
+
+
+def _lowerdir_estimado(deps):
+    """Bytes del string `lowerdir=` que bwrap arma apilando `deps` como capas overlay directas. Es la
+    cantidad de GRAFO (nº y nombre de deps) que cruza el umbral de ENTORNO (una página)."""
+    return _OVERHEAD_OVERLAY + sum(len(_PREF_SANDBOX) + 64 + 1 + len(n) + 1 for n in deps)
+
+
+def patron_overlay(nombre, deps):
+    """¿El apilado directo de las deps desborda el mount de overlay? Si sí, la receta SÓLO monta
+    gracias al merge de deps (cp -al a store/.dmerge, 1 sola capa). El pre-flight hace VISIBLE esa
+    dependencia de una mitigación —y su precondición— que antes era invisible hasta que fallaba."""
+    est = _lowerdir_estimado(deps)
+    if est <= PAGINA_OVERLAY:
+        return None  # entra directo, sin riesgo
+    return dict(
+        patron="overlay-lowerdir",
+        detalle=f"{len(deps)} deps ⇒ lowerdir ~{est}B > {PAGINA_OVERLAY}B (una página)",
+        riesgo="depende-del-merge",
+        nota=("apilado directo DESBORDA ⇒ sólo monta vía el merge de deps (cp -al a store/.dmerge, "
+              "1 capa). Precondición: .dmerge mismo-fs que el store — OK por construcción (vive DENTRO "
+              "del store, fix 6a0dc40). Si el store cambiara de layout y el merge fallara, esta receta "
+              "NO montaría, silenciosamente."))
+
+
+PATRONES_ENTORNO = [patron_overlay]
+
+
+def preflight(objetivo):
+    """Corre los patrones de entorno sobre una receta o (con --perfil) sobre todas las de una imagen,
+    ANTES de construir. Responde la pregunta que kio contestó tarde y cara: ¿este build va a chocar
+    con un límite del entorno que el grafo no ve?"""
+    colas = cargar_colas()
+    idx = {q: set(m) for q, m in colas.items()}
+    if objetivo.startswith("perfil:"):
+        pn = objetivo.split(":", 1)[1]
+        p = targets_mod.load().get(pn)
+        if not p:
+            print(f"perfil desconocido: {pn}", file=sys.stderr); return 1
+        cola = p.get("cola", "corpus")
+        nombres = [(cola if n in idx.get(cola, ()) else "corpus", n)
+                   for n in colas.get(cola, {})] if cola != "corpus" else \
+                  [("corpus", n) for n in colas["corpus"]]
+        # sólo los del ámbito: todas las recetas de la cola del perfil
+        objetivos = [(q, n) for q in ({cola} | {"corpus"}) for n in colas.get(q, {})]
+    else:
+        ubic = [(q, objetivo) for q in colas if objetivo in colas[q]]
+        if not ubic:
+            print(f"no hay receta `{objetivo}` en ninguna cola", file=sys.stderr); return 1
+        objetivos = ubic
+
+    hits = []
+    for (q, n) in objetivos:
+        deps = colas[q][n]
+        for patron in PATRONES_ENTORNO:
+            v = patron(n, deps)
+            if v:
+                hits.append((f"{q}/{n}", v))
+    hits.sort(key=lambda h: -_lowerdir_estimado(colas[h[0].split("/")[0]][h[0].split("/", 1)[1]]))
+
+    if not objetivo.startswith("perfil:"):
+        q, n = objetivos[0]
+        print(f"== preflight de {q}/{n} ({len(colas[q][n])} deps)")
+        if not hits:
+            print("   ✓ sin riesgo de entorno conocido (entra en el mount overlay directo).")
+        for _, v in hits:
+            print(f"   ⚠ {v['patron']}: {v['detalle']}")
+            print(f"      {v['nota']}")
+        return 0
+    print(f"== preflight del perfil (patrones: {', '.join(p.__name__ for p in PATRONES_ENTORNO)})")
+    print(f"   {len(hits)} recetas dependen del merge para montar (apilado directo desbordaría):")
+    for nombre, v in hits[:25]:
+        print(f"   ⚠ {nombre:28} {v['detalle']}")
+    print(f"\n   son construibles HOY (el merge compacta), pero su montaje CUELGA de que store/.dmerge")
+    print(f"   sea mismo-fs que el store. Si un host rompe eso, fallan silenciosas — pre-vuela antes.")
+    return 0
+
+
 def _tiempos():
     """nombre → mediana de segundos de build, desde `store/.times/*.json` (sidecar no determinista,
     lo escribe `scripts/farm/build-timed.sh` en el worker y llega con el rsync de cosecha). Vacío
@@ -614,6 +713,9 @@ def main():
         return duplicados(como_json="--json" in extra)
     if cmd == "keystones":
         return keystones(como_json="--json" in extra)
+    if cmd == "preflight" and extra:
+        obj = f"perfil:{extra[extra.index('--perfil') + 1]}" if "--perfil" in extra else extra[0]
+        return preflight(obj)
     if cmd == "perfiles" and extra:
         memb = membresia()
         hits = {f"{q}/{n}": sorted(ps) for (q, n), ps in memb.items() if n == extra[0]}