yupana: invariante (c) CANÓNICO — detector de duplicados por evidencia de fuente

`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>
This commit is contained in:
2026-07-22 19:59:20 -04:00
co-authored by Claude Opus 4.8
parent e5e3f9e281
commit 0f3a1dc394
3 changed files with 492 additions and 1 deletions
+16 -1
View File
@@ -38,8 +38,22 @@ def main():
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 del radio cruzado:")
print("✗ REGRESIÓN de yupana:")
for f in fallos:
print(" -", f)
return 1
@@ -47,6 +61,7 @@ def main():
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__":