`yupana duplicados` caza nudos que son el MISMO paquete bajo nombres/colas
distintos, agrupando por lo que BAJA (sha/url), no por el nombre. Clasifica:
colisión-fuente — mismo sha, familias de nombre DISTINTAS ⇒ casi seguro un
sha copiado por error. ACCIONABLE.
variante — mismo sha, misma familia (zlib/zlib-shared, mesa-*) ⇒
diversificación deliberada estático/shared, OK (14 vistas).
sombra — mismo nombre en >1 cola; redundante si versión+sha+deps
coinciden (una sobra), justificada si difieren.
HALLAZGO INMEDIATO — el detector encontró un bug real en su primera corrida:
itstool.toml dice versión 2.0.7 pero su url+sha apuntan al tarball de
gettext-tiny (29cc165e…). Construiría la fuente equivocada. radio itstool=1,
gettext-tiny=78 ⇒ el sha copiado es el de itstool.
Otras 3 colisiones son demos (adwaita-hello/libadwaita, sourceview-hello/
gtksourceview, radio 0) o subcomponentes (prison/prison-scanner) — a revisar,
no urgentes. Y 1 sombra redundante: incoming-kde/dbus es byte-idéntica a
corpus/dbus (mismo hash de artefacto) ⇒ los 114 consumidores KDE podrían
apuntar a corpus/dbus sin rebuild.
NO funde nada solo: reporta a docs/state/duplicados.json y deja el juicio al
humano (como el triaje). El radio de cada nudo dice cuál es el canónico (el de
mayor radio). Guardián extendido: test-yupana-radio.py falla si el detector
deja de ver colisiones de fuente (agrupar por nombre en vez de sha = invariante
c ciego).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
3.4 KiB
Python
Executable File
69 lines
3.4 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 de duplicados debe seguir viendo la fuente, no el nombre. La
|
|
# identidad de un nudo es lo que BAJA (sha), no cómo se llama. Si esto se rompe (p.ej. alguien
|
|
# agrupa por nombre), las colisiones de fuente —shas copiados por error— dejan de verse.
|
|
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[1]) # nombres por sha
|
|
colis = {s: ns for s, ns in por_sha.items()
|
|
if len({yupana._base(n) for n in ns}) > 1} # familias distintas, misma fuente
|
|
if not colis:
|
|
fallos.append("el detector de duplicados no ve NINGUNA colisión de fuente — "
|
|
"¿se está agrupando por nombre en vez de por sha? (invariante c ciego)")
|
|
|
|
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 de duplicados por fuente: {len(colis)} colisiones vistas (invariante c activo)")
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|