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>
This commit is contained in:
@@ -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__":
|
||||
|
||||
@@ -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]}
|
||||
|
||||
Reference in New Issue
Block a user