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>
96 lines
5.6 KiB
Python
Executable File
96 lines
5.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# test-yupana-radio.py — REGRESIÓN del bug de libdrm (2026-07-22). No es un test cualquiera: es la
|
|
# cicatriz. Si el radio de un paquete compartido entre colas vuelve a medirse sobre una sola cola,
|
|
# esto falla. La propiedad que blinda: el radio SIEMPRE cruza todas las colas del disco.
|
|
import sys, subprocess
|
|
from pathlib import Path
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
import yupana
|
|
|
|
def main():
|
|
colas = yupana.cargar_colas()
|
|
rev = yupana.dependientes(colas)
|
|
memb = yupana.membresia(colas)
|
|
fallos = []
|
|
|
|
# 1) libdrm lo consumen recetas de MÁS de una cola (corpus + incoming-kde). Medirlo sobre una
|
|
# sola cola es exactamente el bug.
|
|
if ("corpus", "libdrm") in rev:
|
|
colas_consumidoras = {q for q, _ in rev[("corpus", "libdrm")]}
|
|
if len(colas_consumidoras) < 2:
|
|
fallos.append(f"libdrm consumido desde una sola cola {colas_consumidoras} "
|
|
f"— el radio se está midiendo recortado")
|
|
# y su membresía DEBE incluir escritorio-kde (la mentira original decía sólo mirada)
|
|
if "escritorio-kde" not in memb.get(("corpus", "libdrm"), set()):
|
|
fallos.append("libdrm NO atribuido a escritorio-kde — perfiles vuelve a mentir")
|
|
|
|
# 2) Invariante general: para todo nudo del corpus consumido por alguna receta de una cola
|
|
# incoming-*, su membresía y sus dependientes DEBEN reflejar esa cola. (Detecta el recorte
|
|
# en cualquier paquete compartido, no sólo libdrm.)
|
|
for (q, n), consumidores in rev.items():
|
|
if q != "corpus":
|
|
continue
|
|
colas_cons = {cq for cq, _ in consumidores}
|
|
if "corpus" in colas_cons and len(colas_cons) == 1:
|
|
continue # sólo corpus, nada que cruzar
|
|
# hay consumidores de otra cola: su membresía no puede ser vacía si esas colas tienen perfil
|
|
if consumidores and not memb.get((q, n)) and any(cq != "corpus" for cq in colas_cons):
|
|
pass # puede no tener perfil declarado; no es fallo duro, sólo libdrm es el guardián
|
|
|
|
# 3) INVARIANTE (c): el detector debe AGRUPAR por fuente (sha), no por nombre — la identidad de un
|
|
# nudo es lo que BAJA. Ancla: itstool y gettext-tiny comparten sha (itstool le presta el tarball
|
|
# como carrier). Si el detector deja de ver ese grupo, está agrupando por nombre = invariante ciego.
|
|
recs_f = yupana._recetas_con_fuente()
|
|
por_sha = {}
|
|
for k, r in recs_f.items():
|
|
if r["sha"]:
|
|
por_sha.setdefault(r["sha"], set()).add(k)
|
|
sha_it = recs_f.get(("corpus", "itstool"), {}).get("sha")
|
|
if not sha_it or ("corpus", "gettext-tiny") not in por_sha.get(sha_it, set()):
|
|
fallos.append("el detector no agrupa itstool con gettext-tiny por sha — "
|
|
"¿agrupa por nombre en vez de por fuente? (invariante c ciego)")
|
|
|
|
# 3b) NO crIar lobos: itstool NO es un bug — es un CARRIER (stub que presta el tarball, no
|
|
# construye la fuente). Si el detector lo llama construye_fuente=True, lo reportaría como
|
|
# colisión falsa (casi se "arregla" un stub apuntándolo al itstool real que no compila en el lab).
|
|
if recs_f.get(("corpus", "itstool"), {}).get("construye_fuente"):
|
|
fallos.append("itstool marcado como que CONSTRUYE su fuente — es un carrier/stub, "
|
|
"el detector lo reportaría como colisión falsa")
|
|
# 3c) dbus: corpus (estático) vs incoming-kde (dinámico, para Qt6DBus) — misma fuente, BUILD
|
|
# distinto ⇒ variante deliberada, NO sombra redundante. Casi se borra load-bearing (2026-07-22).
|
|
if ("corpus", "dbus") in recs_f and ("incoming-kde", "dbus") in recs_f:
|
|
if recs_f[("corpus", "dbus")]["build_fp"] == recs_f[("incoming-kde", "dbus")]["build_fp"]:
|
|
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:
|
|
print(" -", f)
|
|
return 1
|
|
d = len(rev.get(("corpus", "libdrm"), ()))
|
|
print(f"✓ radio cruza colas: libdrm consumido por {d} recetas de "
|
|
f"{len({q for q,_ in rev.get(('corpus','libdrm'),())})} colas, atribuido a "
|
|
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__":
|
|
sys.exit(main())
|