#!/usr/bin/env python3 """censo-distro.py — qué le falta a la distro para que una persona la use. ── POR QUÉ EXISTE, Y QUÉ PREGUNTA CONTESTA QUE LAS OTRAS NO ──────────────────────────────────── El repo ya mide tres cosas y ninguna es ésta: · `build-state*.json` ¿lo DECLARADO está construido? (hoy: 1158/1158, deuda 0) · `frontera-triaje.toml` ¿la clausura de build tiene huecos? (cerrada, 362/365 decididos) · `cierres.json` ¿cada perfil cierra? (hoy: 10 de 10) Las tres contestan **«¿está completo lo que ya nombramos?»**. Ninguna contesta **«¿alcanza para usarla?»**, y por eso el tablero podía estar entero en verde mientras la distro no sabía mostrar un carácter chino. Lo dijo el operador el 2026-10-16: *«no hay qué granjear desde lo que tenemos. pero lo que no tenemos, faltan montón de cosas»*. Tenía razón y no existía el número. ⚠ **LA LISTA DE ABAJO ES JUICIO, NO MEDIDA**, igual que la columna `destino` de `busybox-applets.tsv` — y conviene que esté dicho en la primera línea del fichero. Lo que SÍ se mide es cada renglón: si hay receta (en el catálogo, en una cola o en `tandas/`) y si viaja en alguna imagen. Agregar o quitar un renglón es una decisión de producto; el guion sólo dice en qué estado está lo que alguien ya decidió que importa. ⚠ **EL PARENTESCO DE NOMBRE ES UNA PISTA, NO UN VEREDICTO — y esto se corrigió DOS VECES.** La v1 cantaba huecos falsos: «falta `clang`» con `clang18` en el catálogo, «falta `rustc`» con `rust`, «falta `neomutt`» con `mutt`. Se arregló buscando un pariente de nombre y sacando esos renglones del recuento… y eso fue **peor**, porque entonces el censo escondía huecos REALES: `neovim` salía como «revisar → vim» (y neovim falta de verdad: existe `bob-nvim`, su gestor de versiones, sin neovim), `swaync` → `sway`, `patchelf` → `patch`, `man-pages` → `mandoc` (que es el LECTOR, no el contenido). ⇒ **Un pariente ANOTA, no absuelve.** Un candidato sin receta cuenta como `falta`, siempre; si hay un nombre parecido se añade como `pista` para que nadie repita la búsqueda. Las dos formas de mentir tienen el mismo antídoto: que la pista viaje al lado del hueco en vez de reemplazarlo. Uso: censo-distro.py [--json docs/state/distro-huecos.json] """ import argparse import glob import importlib.util import json import os import sys RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(RAIZ, 'scripts')) # Cada entrada: categoría → (por_qué_importa, [nombres candidatos]). # El «por qué» no es adorno: un renglón sin motivo se discute para siempre. CHECKLIST = { # ── LO QUE DEJA A GENTE AFUERA ────────────────────────────────────────────────────────────── 'escritura del mundo': ( 'las únicas fuentes del corpus son dejavu y su nerd: sin estas, la distro muestra ' 'cuadraditos en chino, japonés, coreano, árabe, hindi, tailandés y emoji', ['noto-fonts-cjk', 'noto-fonts-emoji', 'noto-fonts', 'noto-fonts-extra']), 'escribir en esos idiomas': ( 'una fuente deja LEER; para ESCRIBIR hace falta un método de entrada. ✅ El marco `fcitx5` ' 'y el motor COREANO entraron el 2026-10-20 (con `libhangul`, que viaja por su diccionario ' 'hanja: la ruta está horneada en la .a). Faltan los otros idiomas. ' '⚠ Los renglones se nombran como los PAQUETES de fcitx5, no como las bibliotecas motor: mi ' 'primera versión pedía `anthy`, `libpinyin` y `hangul`, y con `fcitx5-hangul` ya sellado el ' 'censo seguía contando «hangul» como hueco — un hueco falso por nombrar la pieza de abajo ' 'en vez de la que se declara', ['fcitx5', 'ibus', 'fcitx5-hangul', 'fcitx5-anthy', 'fcitx5-chinese-addons', 'fcitx5-rime']), 'accesibilidad': ( 'sin lector de pantalla ni voz la distro es inusable para quien no ve', ['orca', 'speech-dispatcher', 'espeak-ng', 'brltty']), # ── LO QUE ALGUIEN BUSCA EL PRIMER DÍA ────────────────────────────────────────────────────── 'terminales populares': ( '`foot` está y alcanza; pero quien llega de otra distro busca el suyo por nombre, y el ' 'doc 20 §II.2 ya los listaba como «barato». kitty: 13 de sus 14 deps ya están en el ' 'catálogo (falta librsync, opcional)', ['kitty', 'alacritty', 'wezterm', 'ghostty', 'xterm']), 'editores': ( '`vim` y `helix` están. ⚠ Existe `bob-nvim` —el GESTOR DE VERSIONES de neovim— y no ' 'existe neovim: el conductor sin el motor, como `crun` sin podman', ['neovim', 'emacs', 'micro', 'kakoune', 'nano']), 'shells': ('bash, zsh y nushell están', ['fish', 'elvish']), 'lanzador, notificaciones y bloqueo': ( 'el esqueleto de un escritorio Wayland que no sea GNOME/KDE: sin lanzador no se abre nada', ['rofi', 'wofi', 'fuzzel', 'bemenu', 'tofi', 'mako', 'dunst', 'swaync', 'swaylock', 'gtklock', 'waylock']), 'audio de escritorio': ( 'PipeWire suena y no hay con qué subirle el volumen desde una ventana', ['pavucontrol', 'pamixer', 'playerctl', 'easyeffects', 'wiremix']), 'capturas y portapapeles': ('grim, slurp, wl-clipboard y cliphist ya están', ['satty', 'swappy', 'flameshot', 'wtype', 'kanshi']), # ✅ CERRADO el 2026-10-20 con `swayimg`, y la fila cambia de forma a propósito: decía «5 de 5 # ausente» listando los cinco visores famosos, y **ninguno de los cinco puede funcionar acá**. # Dejar esa fila en rojo invitaba a una campaña imposible; lo honesto es que el candidato que SÍ # sirve esté en la lista y que los muros de los otros queden escritos (ver `MUROS`). 'visores de imagen': ('swayimg está: png/jpeg/webp/tiff/gif + rotación EXIF, y modo DRM para ' 'consola. Los cinco de abajo están TAPADOS, ver MUROS', ['swayimg', 'imv', 'nsxiv', 'feh', 'loupe', 'eog']), # ── CAPACIDADES ENTERAS SIN NINGÚN CAMINO ─────────────────────────────────────────────────── 'archivos comprimidos ajenos': ( '⚠ EL ARGUMENTO DE ESTA FILA ERA FALSO Y LO MEDÍ DESPUÉS: decía «no se puede abrir un .zip» ' 'porque `libzip` es una biblioteca y no el comando. Cierto para libzip — y falso como ' 'conclusión: `libarchive` estaba SELLADO y trae `bsdunzip`/`bsdtar`, verificados con un zip ' 'real en lectura Y escritura. Se declaró en `cli` el 2026-10-19. Lo que falta son los ALIAS ' '(`unzip`, `zip`) para que un script ajeno que los invoque no falle, y 7z/rar sin medir', ['unzip', 'zip', 'p7zip', '7zip', 'unrar']), 'imprimir de verdad': ('cups solo no imprime: los filtros y los drivers son otro paquete', ['cups', 'cups-filters', 'ghostscript', 'gutenprint', 'hplip']), 'escanear': ('ningún camino hoy', ['sane-backends', 'simple-scan']), 'documentos técnicos': ('nada que componga un PDF desde texto marcado', ['texlive', 'pandoc', 'typst', 'groff']), 'páginas de manual': ('`mandoc` LEE man; falta el CONTENIDO y el formateador de info', ['man-pages', 'texinfo']), 'virtualización': ('ni invitado ni anfitrión: no se puede probar la propia ISO sin salir', ['qemu', 'libvirt', 'virt-manager']), 'contenedores': ('`crun` está (el motor); falta quien lo conduzca', ['podman', 'docker', 'buildah', 'flatpak', 'distrobox']), 'java': ('un ecosistema entero queda fuera sin una JVM', ['openjdk', 'jdk', 'temurin']), 'bases de datos de servidor': ('sqlite está; un servicio con varios clientes no', ['postgresql', 'mariadb', 'redis', 'valkey']), 'depurar y medir': ('se construye la distro entera y no hay con qué mirar un core', ['gdb', 'lldb', 'valgrind', 'ltrace', 'perf', 'patchelf']), 'discos y salud del equipo': ('nada que avise de un disco muriéndose', ['smartmontools', 'nvme-cli', 'hdparm', 'gparted']), # ── LENGUAJES Y HERRAMIENTAS DE CONSTRUCCIÓN ──────────────────────────────────────────────── 'otros lenguajes': ( 'python3, lua, go, rust y perl están. ⚠ `zig` NO es receta: vive como artefacto ' '`seed-zig` fuera del catálogo, y es el compilador de TODO el corpus', ['nodejs', 'deno', 'bun', 'ruby', 'php', 'julia', 'ghc', 'ocaml', 'elixir', 'erlang']), 'empaquetadores de python': ('pip no viaja y no hay forma de instalar una herramienta python', ['pipx', 'uv', 'poetry']), 'construcción C clásica': ( '⚠ ESTA FILA ES LA MÁS DUDOSA DEL CHECKLIST y conviene que lo diga: cmake, meson, make y ' 'samurai existen y NO viajan en ninguna imagen — probablemente a propósito, porque una ' 'imagen de producto no se construye a sí misma. La receta de `nodejs` ya tomó esa decisión ' 'por escrito para su caso («acá Node es un compilador más, no software que la distro ' 'publique»). ⇒ lo que falta acá puede no ser un hueco sino un PERFIL de desarrollo que no ' 'existe. Se deja medido y sin declarar. `ninja` además lo instala `samurai` con ese nombre ' 'exacto (1.9.0, medido). Y `gcc` está RETENIDO a propósito (sólo `gcc-libs`), `clang` es ' '`clang18`: eso no es hueco, es decisión', ['autoconf', 'automake', 'ninja']), # ── RED, SEGURIDAD, DATOS ─────────────────────────────────────────────────────────────────── 'vpn y red gestionada': ('networkmanager, wireguard-tools y wpa_supplicant están', ['openvpn', 'iwd', 'nmtui', 'impala', 'mosh', 'sshfs']), 'bluetooth usable': ('`bluez` viaja; falta con qué emparejar un auricular', ['blueman', 'bluetuith']), 'contraseñas': ('nada: la gente guarda sus claves en un .txt o no usa la distro', ['keepassxc', 'pass', 'gopass']), 'respaldos': ('restic y rclone están', ['borgbackup', 'rsnapshot']), 'descargas y torrent': ('curl y wget están', ['aria2', 'transmission', 'qbittorrent']), # ── APLICACIONES DE ESCRITORIO ────────────────────────────────────────────────────────────── 'navegadores': ('firefox y atuq están; el motor Blink no tiene ningún camino', ['chromium', 'qutebrowser', 'librewolf', 'brave']), 'mensajería': ('ningún cliente de chat: ni XMPP, ni Matrix, ni los propietarios', ['dino', 'gajim', 'element-desktop', 'telegram-desktop', 'signal-desktop']), 'música': ('mpv y cmus están para reproducir un fichero; falta biblioteca y daemon', ['mpd', 'ncmpcpp', 'rhythmbox', 'audacious']), 'video': ('mpv está', ['vlc', 'celluloid', 'totem']), 'imagen y diseño': ( '⚠ la suite ArtCraft propia cubre esto con 12 apps (photocraft, vectorcraft, …): estos ' 'renglones son los nombres AJENOS que alguien busca, no un hueco de capacidad', ['gimp', 'inkscape', 'krita', 'darktable']), 'gestores de archivos': ('yazi, nnn, lf, broot y dolphin/nautilus están', ['thunar', 'ranger']), 'lectores de PDF': ('zathura y okular están', ['mupdf', 'sioyek', 'evince']), 'notas': ('ninguno', ['obsidian', 'logseq', 'joplin', 'zettlr']), # ── DECISIONES ABIERTAS, NO OLVIDOS ───────────────────────────────────────────────────────── 'otros compositores Wayland': ( 'hay cinco escritorios (mirada, sway, cosmic, gnome, kde); éstos son los que la gente ' 'nombra y no están. Decisión de alcance, no olvido', ['hyprland', 'river', 'niri', 'labwc', 'wayfire']), 'juegos': ('decisión abierta: arrastra Vulkan, que la distro hoy no publica', ['wine', 'steam', 'gamescope', 'lutris', 'mangohud']), 'infra ajena': ('decisión abierta: son herramientas de operar OTRA cosa', ['kubectl', 'helm', 'terraform', 'ansible']), } # ── CAPACIDAD YA CUBIERTA CON OTRO NOMBRE ────────────────────────────────────────────────────── # Lo simétrico de `MUROS`: el renglón falta POR NOMBRE y la capacidad SÍ está, medida. No absuelve # el hueco —un script que invoque `unzip` sigue fallando, y el censo hace bien en contarlo— pero # cambia el presupuesto de «no hay camino» a «faltan los alias», que es otra cosa y mucho más # barata. Sólo entra lo verificado ejecutando, no lo que upstream promete. CUBIERTOS = { 'unzip': ('libarchive', 'bsdunzip -o x.zip extrae y conserva el árbol — probado con un zip ' 'deflate con subdirectorio el 2026-10-19'), 'zip': ('libarchive', 'bsdtar -a -cf x.zip … crea un zip que python zipfile lee y verifica ' '— probado el 2026-10-19'), } # ── POR QUÉ ESTÁ CADA HUECO: receta ausente ≠ capacidad ausente ───────────────────────────────── # «Falta la receta» y «la distro no publica eso» son dos cosas con presupuestos distintos, y el # censo mentía por omisión al contarlas juntas. Sólo entran acá los muros MEDIDOS, con el dato que # los prueba; lo demás se queda sin anotar, que es más honesto que adivinar. MUROS = { # ── lectores de PDF: la capacidad ESTÁ (zathura en 3 escritorios, okular en KDE) ───────────── 'mupdf': ('sin-visor-wayland', 'medido en su Makefile: sus únicos visores son mupdf-gl (GLUT/OpenGL) y ' 'mupdf-x11/-curl. No hay visor Wayland, y este corpus no publica GL de escritorio. ' 'Su CLI `mutool` SÍ funcionaría: lo que falta no es leer un PDF, es mostrarlo con mupdf'), 'evince': ('introspeccion-gnome', 'GNOME + introspección, igual que loupe y eog; librsvg sólo vive en incoming-gnome'), # ── visores de imagen: los cinco famosos están tapados, y por DOS razones distintas ────────── # Medido el 2026-10-20 al cerrar la fila con `swayimg`. Esto está acá para que nadie vuelva a # intentar el «obvio»: el muro no es que falten deps, es la arquitectura de esta distro. 'imv': ('sin-opengl-de-escritorio', 'sus dependency() piden egl, gl, opengl, glu y wayland-egl, y este corpus no publica ' 'libGL.so ni gl.pc (comprobado en el store). Mismo muro que kitty'), 'nsxiv': ('wayland-only', 'fork de sxiv, Xlib puro. La distro es Wayland-only (ver frente-gnome: X11 al tacho)'), 'feh': ('wayland-only', 'Xlib puro, igual que nsxiv'), 'loupe': ('introspeccion-gnome', 'GTK4 + librsvg + introspección; librsvg sólo existe en incoming-gnome y promover esa ' 'raíz cuesta 35 recetas (medido promoviendo y revirtiendo)'), 'eog': ('introspeccion-gnome', 'igual que loupe, y además es la generación anterior de GNOME'), # ✅ `fcitx5` ESTUVO ACÁ Y SE FUE: el muro se derribó el 2026-10-19/20. Decía # `corpus-estatico-sin-pic` con 7365 errores de PIC medidos, y era cierto — hasta que el # operador decidió publicar `libpng`/`freetype`/`fontconfig` con `--with-pic` (40 recetas de # cascada, las 5 imágenes de escritorio). Con eso **fcitx5 SELLA**: daemon + 17 addons, mismo # hash en el hub y en el worker. Se quita de `MUROS` en el mismo movimiento, porque un muro # derribado que sigue anotado es exactamente la clase de pendiente inventado que este fichero # existe para no producir — y hoy ya me pasó con el `.conf` de los emoji. # ⇒ fcitx5 pasa a `tiene-sin-viajar`, que es la verdad: construido y en ningún perfil, porque # el marco sin un motor de idioma daría una barra de IME que no convierte nada. 'kitty': ('sin-opengl-de-escritorio', "setup.py:804 hace pkg_config('gl', '--libs') y data-types.h:25 exige OpenGL >= 3.x " "core. Medido en el store: NADIE publica `gl.pc` ni `libGL.so` — mesa da egl.pc, " "glesv2.pc, gbm.pc y dri.pc; libglvnd da opengl.pc con libOpenGL.so/libGLdispatch.so " "y sólo viaja en escritorio-kde. Compilar no basta: gl.c:68 aborta en runtime con " "«OpenGL version is X.Y, version >= 3.3 required for kitty». Mismo muro que OBS."), } # ── ¿LO INSTALA YA ALGÚN ARTEFACTO VIGENTE, CON ESE MISMO NOMBRE? ────────────────────────────── # Automatiza el hallazgo que destapó `libarchive`: un renglón puede faltar como RECETA y existir # como BINARIO dentro de otro paquete. Se mide contra los `bin`/`sbin`/`usr/bin`/`usr/sbin` de los # artefactos sellados, igual que `busybox-censo-proveedores.py`. # # ⚠ SÓLO ARTEFACTOS VIGENTES, y es un defecto medido de mi primera versión (2026-10-19): barriendo # `/store/*` entero, `unzip` y `hdparm` salían «ya los instala busybox» — y era el busybox # SUPERADO (`2a2b1280…`). El vigente (`4bd934f6…`) ya no los trae, porque la campaña de botar # busybox los recortó. **Un artefacto superado no dice nada de lo que hay en una imagen**, y # contarlo convierte el instrumento que caza huecos falsos en una fábrica de huecos falsos. # Es la misma lección que los «sellados rancios» del static-audit. # # ⚠ Y los rootfs ENSAMBLADOS se excluyen por nombre: traen el busybox del piso adentro, así que # todo applet se «proveería» a sí mismo. Mismo criterio que el censo de applets. ROOTFS = ('product-rootfs', 'product-attested-rootfs', 'stage1-rootfs') DIRS_BIN = ('bin', 'sbin', 'usr/bin', 'usr/sbin') def _cargar(nombre, ruta): """Se importa por RUTA porque los ficheros llevan guion y no son módulos importables.""" spec = importlib.util.spec_from_file_location(nombre, ruta) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def indice_de_binarios(cp): """{nombre de binario: {recetas VIGENTES que lo instalan}}.""" hashes, _ = cp.mapa_hashes() vigentes = {} for (cola, nombre), (h, estado) in hashes.items(): if not h or estado != 'sealed' or nombre in ROOTFS: continue vigentes.setdefault(nombre, (h or '').replace('b3:', '')) idx = {} for nombre, h in vigentes.items(): base = os.path.join('/store', '%s-%s' % (h, nombre)) for sub in DIRS_BIN: d = os.path.join(base, sub) if not os.path.isdir(d): continue try: entradas = os.listdir(d) except OSError: continue for e in entradas: idx.setdefault(e, set()).add(nombre) return idx def nombres_conocidos(): """Toda receta alcanzable: catálogo, colas `incoming-*` y `tandas/`.""" n = set() for f in glob.glob(os.path.join(RAIZ, 'recipes/*.toml')): n.add(os.path.basename(f)[:-5]) for f in glob.glob(os.path.join(RAIZ, 'recipes/incoming-*/*.toml')): n.add(os.path.basename(f)[:-5]) for f in glob.glob(os.path.join(RAIZ, 'tandas/**/*.toml'), recursive=True): n.add(os.path.basename(f)[:-5]) return n def clasificar(cand, conocidos, en_imagen): """`tiene-y-viaja` · `tiene-sin-viajar` · `falta` (+ pistas de nombre parecido, que NO absuelven).""" if cand in conocidos: return ('tiene-y-viaja' if cand in en_imagen else 'tiene-sin-viajar'), [] raiz = cand.split('-')[0] # Un pariente es: una receta cuyo nombre EMPIEZA por la raíz del candidato (`cups` → `cups`, # `steam` → `steampipe`), o una receta que es PREFIJO o SUFIJO del candidato (`mutt` dentro de # `neomutt`). Y no «contenida en cualquier posición», que es lo que hacía la primera versión: # con eso `age` salía pariente de `virt-manager` y `patch` de `speech-dispatcher`. Un pariente # falso no es inocuo —manda a revisar algo que no hay que revisar— aunque sea menos grave que # un hueco falso. Raíz de 3+ caracteres: con 2 todo se parece a todo. cerca = sorted(x for x in conocidos if (len(raiz) >= 3 and x.startswith(raiz)) or (len(x) >= 3 and (cand.startswith(x) or cand.endswith(x)))) return 'falta', cerca[:4] def main(): ap = argparse.ArgumentParser() ap.add_argument('--json') a = ap.parse_args() import yupana cp = _cargar('cierre_perfiles', os.path.join(RAIZ, 'scripts', 'cierre-perfiles.py')) conocidos = nombres_conocidos() en_imagen = {n for (c, n) in yupana.membresia_imagen() if c != 'wanted'} try: binarios = indice_de_binarios(cp) except Exception as e: # sin store legible el censo sigue sirviendo; se dice y se sigue print(' (no pude indexar los binarios del store: %s)' % e) binarios = {} filas, resumen = [], {'tiene-y-viaja': 0, 'tiene-sin-viajar': 0, 'falta': 0} for cat, (porque, cands) in CHECKLIST.items(): items = [] for c in cands: estado, cerca = clasificar(c, conocidos, en_imagen) resumen[estado] += 1 items.append({'nombre': c, 'estado': estado, 'parientes': cerca}) filas.append({'categoria': cat, 'porque': porque, 'items': items, 'faltan': [i['nombre'] for i in items if i['estado'] == 'falta']}) for i in items: if i['estado'] == 'falta' and i['parientes']: i['pista'] = 'nombre parecido en el catálogo (verificar, NO es equivalencia)' if i['nombre'] in MUROS: i['muro'], i['muro_porque'] = MUROS[i['nombre']] if i['nombre'] in CUBIERTOS: i['cubierto_por'], i['cubierto_porque'] = CUBIERTOS[i['nombre']] elif i['estado'] == 'falta' and i['nombre'] in binarios: quien = sorted(binarios[i['nombre']]) i['cubierto_por'] = ', '.join(quien) i['cubierto_porque'] = ('MEDIDO: un artefacto vigente instala un binario con ese ' 'nombre exacto. Falta la receta propia, no la capacidad') print('== qué le falta a la distro para usarse (checklist de JUICIO, estado MEDIDO) ==') print(' recetas alcanzables: %d · de ellas en alguna imagen: %d' % (len(conocidos), len(en_imagen))) for f in sorted(filas, key=lambda x: -len(x['faltan'])): marca = '✗' if f['faltan'] else '·' print(' %s %-28s %d/%d falta%s' % (marca, f['categoria'], len(f['faltan']), len(f['items']), ': ' + ', '.join(f['faltan']) if f['faltan'] else 'n')) # ⚠ **EL `porque` SE IMPRIME, Y ESTE ARREGLO SALIÓ DE CAER EN LA TRAMPA.** # Cada fila guarda desde siempre un campo que dice qué hay YA cubierto o por qué importa # —`'lectores de PDF': ('zathura y okular están', [...])`— y **no se mostraba nunca**. La # salida decía `✗ lectores de PDF 3/3 falta: mupdf, sioyek, evince`, que se lee como «no hay # lector de PDF»… y el 2026-10-20 me puse a buscar candidatos para construir uno, hasta que # al mirar el fichero vi que zathura ya estaba sellada y viajando en tres escritorios. # # O sea: el dato que evitaba una campaña inútil estaba en el censo y no llegaba al ojo. # Una métrica que guarda el matiz y muestra sólo el número rojo invita exactamente al # trabajo que debía evitar. if f['faltan'] and f['porque']: print(' ↳ ya cubierto / contexto: %s' % f['porque']) rev = [(i['nombre'], i['parientes']) for f in filas for i in f['items'] if i['estado'] == 'falta' and i['parientes']] if rev: print(' — PISTAS: falta, pero hay un nombre parecido (verificar; NO es equivalencia):') for n, p in rev: print(' %-20s ~ %s' % (n, ', '.join(p))) cub = [(i['nombre'], i['cubierto_por']) for f in filas for i in f['items'] if i.get('cubierto_por')] if cub: print(' — CAPACIDAD YA CUBIERTA con otro nombre (falta el alias, no el camino):') for n, q in cub: print(' %-20s lo hace %s' % (n, q)) con_muro = [(i['nombre'], i['muro']) for f in filas for i in f['items'] if i.get('muro')] if con_muro: print(' — MUROS MEDIDOS (no es que falte la receta: la distro no publica eso):') for n, m in con_muro: print(' %-20s %s' % (n, m)) print(' resumen: %s' % json.dumps(resumen, ensure_ascii=False)) if a.json: with open(a.json, 'w', encoding='utf-8') as fh: json.dump({'schema': 'takana-censo-distro/1', 'recetas_alcanzables': len(conocidos), 'en_alguna_imagen': len(en_imagen), 'resumen': resumen, 'categorias': filas}, fh, ensure_ascii=False, indent=1, sort_keys=True) fh.write('\n') print(' → %s' % a.json) return 0 if __name__ == '__main__': sys.exit(main())