#!/usr/bin/env python3 # seed-gnome.py — siembra el SUBTREE transitivo de GNOME desde metadata de nixpkgs (NO construye con # nix, ADR 0004: sólo lee deps). Corre `sembrar_cierre` de seed-graph sobre las raíces de una sesión # GNOME y clasifica el cierre: # conocida → ya hay receta en el corpus (cache-hit potencial) # frontera → nixpkgs lo pide y hammer NO tiene receta → HAY QUE AUTORAR (esto es el trabajo real) # nix-ismo/lab → ruido de nixpkgs o lo provee el sandbox (se descarta) # # Cada `frontera` es una HIPÓTESIS a clasificar/autorar por un humano (o un subagente-por-receta), NO # se promueve a receta automáticamente. Salida durable a docs/state/seed-gnome.json. import json, sys, pathlib, importlib.util # el guion en `seed-graph.py` impide `import seed_graph`: lo cargo por ruta. _spec = importlib.util.spec_from_file_location( "seed_graph", str(pathlib.Path(__file__).parent / "seed-graph.py")) sg = importlib.util.module_from_spec(_spec); _spec.loader.exec_module(sg) ROOT = pathlib.Path(__file__).resolve().parent.parent OUT = ROOT / "docs/state/seed-gnome.json" # Raíces de una sesión GNOME usable: el compositor (mutter), el shell y su runtime JS (gjs), la # sesión y sus daemons, el display manager, el portal, y las apps mínimas (files, terminal, ajustes). # El cierre transitivo trae el resto (mozjs bajo gjs, gcr/libsecret bajo el keyring, etc.). RAICES = [ "mutter", "gnome-shell", "gjs", "gnome-session", "gnome-settings-daemon", "gsettings-desktop-schemas", "gnome-desktop", "gnome-menus", "gnome-keyring", "gdm", "xdg-desktop-portal", "xdg-desktop-portal-gnome", "libadwaita", "nautilus", "gnome-console", "gnome-control-center", "gnome-text-editor", ] MINIMA = [ # lo que una sesión GNOME necesita para BOOTEAR a un shell usable (sin apps) "mutter", "gnome-shell", "gjs", "gnome-session", "gnome-settings-daemon", "gsettings-desktop-schemas", "gnome-desktop", "gdm", "xdg-desktop-portal-gnome", ] # ── LA OPTIMIZACIÓN (el "yupana de gnome", no un sembrador crudo). El cierre de metadata de nixpkgs # SOBREESTIMA ~5-10× la frontera real de hammer, porque declara buildInputs de features MÁXIMAS. Estas # tablas son HIPÓTESIS de triage (NO verdad — la verdad emerge autorando feature-minimal), para que el # reporte no mienta con "465 recetas". Cada una vuelve legible por qué un candidato probablemente NO es # hueco. Clasificar a mano al autorar; mover entre cubetas es esperable. SUSTITUCION = { # hammer YA tiene equivalente → mapeo, no hueco "systemd", "systemd-minimal", "systemd-minimal-libs", # → elogind "libpulseaudio", # → pipewire (libpulse compat) "networkmanager", # → iwd / systemd-networkd (a elegir) } TOOLING = { # generadores de docs / build-glue: el lab los provee o se deshabilitan "vala", "gi-docgen", "gtk-doc", "docutils", "doxygen", "texinfo", "autoconf", "automake", "autoconf-archive", "xmlto", "nasm", "yasm", "cmocka", "gtest", "bash-completion", "desktop-file-utils", "gettext", "itstool", "sassc", "meson", "ninja", "help2man", "gperf", "appstream", "appstream-glib", "asciidoc", "docbook-xsl", "docbook-xml", "docbook-sgml", "docbook5", "perl", "python3", "gobject-introspection", "sphinx", "graphviz", "groff", "swig", "dejagnu", "expect", "valgrind", "gdb", "man-db", "po4a", "ragel", "byacc", "gengetopt", "validate-pkg-config", "prune-libtool-files", } # subárboles de features/apps que una sesión mínima NO necesita (defer): media, impresión, red, # almacenamiento, TPM, escáner, runtimes de scripting arrastrados por deps opcionales. OPCIONAL_PREF = ( "ffmpeg", "gst-plugins-bad", "gst-plugins-ugly", "gst-libav", "x264", "x265", "svt-av1", "dav1d", "libaom", "libvpx", "fdk-aac", "libmad", "libmpg123", "mpg123", "opusfile", "wavpack", "libtheora", "vorbis", "zimg", "vid.stab", "libass", "srt", "zvbi", "sbc", "ldacbt", "liblc3", "samba", "talloc", "tevent", "tdb", "ldb", "libnfs", "libiscsi", "cyrus-sasl", "net-snmp", "unbound", "cups", "sane", "colord", "argyllcms", "bluez", "freerdp", "evolution", "gnome-online-accounts", "gnome-remote-desktop", "webkitgtk", "gnome-clocks", "flatpak", "ostree", "gvfs", "openjdk", "ruby", "lua", "tcl", "tpm2", "swtpm", "libtpms", "cryptsetup", "lvm2", "libnvme", "libndctl", "mariadb", "psqlodbc", "unixodbc", "openldap", "krb5", "sphinxcontrib", "geoclue", "libcamera", "roc-toolkit", "exiv2", "poppler", ) def _cubeta(dep): if dep in SUSTITUCION: return "sustitucion" if dep in TOOLING: return "tooling" if any(t in dep for t in OPCIONAL_PREF): return "opcional" return "espinazo" def main(): recs = sg.recetas() raices = MINIMA if (len(sys.argv) > 1 and sys.argv[1] == "--minima") else RAICES print(f"== raíces GNOME: {len(raices)}", file=sys.stderr) grafo = sg.sembrar_cierre(raices, recs, prof_max=6, tope=4000) out_path = ROOT / ("docs/state/seed-gnome-minima.json" if raices is MINIMA else "docs/state/seed-gnome.json") # clasificar TODO el cierre clases = {"conocida": [], "frontera": [], "nix-ismo": [], "lab": [], "sin-attr": []} for n, aristas in grafo.items(): if aristas is None: clases["sin-attr"].append(n); continue # nodos del cierre = todo lo referido como dep, clasificado por su presencia todos_deps = {} for n, aristas in grafo.items(): for a in (aristas or []): d = a["dep"] # el nodo se clasifica por su MEJOR evidencia (conocida > frontera) prev = todos_deps.get(d) rank = {"conocida": 3, "lab": 2, "nix-ismo": 1, "frontera": 0} if prev is None or rank[a["clase"]] > rank[prev["clase"]]: todos_deps[d] = a conocidas = sorted(d for d, a in todos_deps.items() if a["clase"] == "conocida") frontera = sorted(d for d, a in todos_deps.items() if a["clase"] == "frontera") nixismos = sorted(d for d, a in todos_deps.items() if a["clase"] == "nix-ismo") lab = sorted(d for d, a in todos_deps.items() if a["clase"] == "lab") sin_attr = sorted(clases["sin-attr"]) # ¿cuántas veces piden cada frontera? (prioridad de autoría) demanda = {} for n, aristas in grafo.items(): for a in (aristas or []): if a["clase"] == "frontera": demanda.setdefault(a["dep"], []).append(n) print(f"\n== SUBTREE GNOME (cierre transitivo, {len(grafo)} nodos visitados)") print(f" ya tenemos receta (conocida): {len(conocidas)}") print(f" FRONTERA a autorar: {len(frontera)}") print(f" nix-ismos (ruido): {len(nixismos)}") print(f" provisto por el lab: {len(lab)}") print(f" sin attr en nixpkgs: {len(sin_attr)}" + (f" ({' '.join(sin_attr[:8])}…)" if sin_attr else "")) # ── TRIAGE (hipótesis): parte la frontera cruda en cubetas para no mentir con el conteo bruto. cubetas = {"sustitucion": [], "tooling": [], "opcional": [], "espinazo": []} for d in frontera: cubetas[_cubeta(d)].append(d) espinazo = sorted(cubetas["espinazo"], key=lambda k: (-len(demanda.get(k, [])), k)) # keystone / palo largo: gjs bloquea gnome-shell, y gjs cuelga de spidermonkey(mozjs). palo_largo = [d for d in ("spidermonkey", "gjs", "mozjs") if d in frontera] print(f"\n== TRIAGE de la frontera cruda (HIPÓTESIS — el cierre de nixpkgs sobreestima ~5-10×):") print(f" sustitución (systemd→elogind…): {len(cubetas['sustitucion'])}") print(f" tooling/docs (lab o disable): {len(cubetas['tooling'])}") print(f" opcional (media/print/red/…): {len(cubetas['opcional'])}") print(f" ⇒ ESPINAZO candidato a autorar: {len(espinazo)} (SIGUE siendo cota ALTA)") print(f" ⚠ palo largo / keystone: {' → '.join(palo_largo) or '—'} (bloquea gnome-shell)") print(f"\n== espinazo por demanda — top 30 (hipótesis, clasificar al autorar):") for d in espinazo[:30]: print(f" {len(demanda.get(d, [])):3}× {d}") doc = dict( schema="hammer-seed-gnome/2", fuente="nixpkgs metadata (NO se construye con nix — ADR 0004)", advertencia=("El cierre de metadata SOBREESTIMA ~5-10× la frontera real de hammer (buildInputs " "de features máximas). Las cubetas son HIPÓTESIS, no verdad: la frontera real " "emerge autorando TOP-DOWN feature-minimal. NO promover a receta automáticamente."), raices=raices, resumen=dict(visitados=len(grafo), conocida=len(conocidas), frontera_cruda=len(frontera), nix_ismo=len(nixismos), lab=len(lab), sin_attr=len(sin_attr), sustitucion=len(cubetas["sustitucion"]), tooling=len(cubetas["tooling"]), opcional=len(cubetas["opcional"]), espinazo=len(espinazo)), palo_largo=palo_largo, espinazo=espinazo, triage=cubetas, conocida=conocidas, frontera=frontera, nix_ismo=nixismos, lab=lab, sin_attr=sin_attr, demanda={d: sorted(set(q)) for d, q in sorted(demanda.items(), key=lambda kv:(-len(kv[1]),kv[0]))}, ) out_path.write_text(json.dumps(doc, indent=1, ensure_ascii=False) + "\n") print(f"\n → {out_path.relative_to(ROOT)}") return 0 if __name__ == "__main__": sys.exit(main())