gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1506Shell 186Rust 107Markdown 983639 Definiciones

feat(distro): qué le falta para USARSE — la pregunta que ninguna métrica contestaba

El repo medía tres cosas y ninguna era é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 sabe mostrar un
carácter chino. Lo dijo el operador: *«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. Ahora existe: **45 huecos
reales** sobre un checklist de 54 renglones, y de las 1375 recetas alcanzables sólo **592 viajan en
alguna imagen**.

Lo que sale primero, por lo que bloquea:

    escritura del mundo     4/4   noto-fonts-cjk, -emoji, -fonts, -extra  ← la distro muestra
                                  cuadraditos en chino, japonés, coreano, árabe, hindi y emoji
    escribir esos idiomas   5/6   fcitx5, anthy, libpinyin, hangul, rime  ← y tampoco se pueden
                                  TECLEAR: una fuente deja leer, no escribir
    archivos ajenos         4/5   unzip, p7zip, 7zip, unrar               ← un .zip adjunto no abre
    accesibilidad           3/4   orca, espeak-ng, brltty                 ← inusable sin vista
    imprimir de verdad      3/5   ghostscript, gutenprint, hplip          ← cups solo no imprime
    java                    3/3   · bases de datos 3/4 · contenedores 3/4 (crun está, falta quien
    lo conduzca) · virtualización 3/3 · discos 3/3 · LaTeX 2/3 · escanear 1/2 · juegos 3/4

⚠ LA LISTA ES JUICIO, NO MEDIDA — igual que la columna `destino` de `busybox-applets.tsv`, y está
dicho en la primera línea del fichero. Lo medido es el ESTADO de 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
decisión de producto.

⚠ Y no cuenta un hueco sin buscarle un pariente de nombre. Es el error que cometí dos veces hoy:
la primera versión decía «falta clang» con `clang18` en el catálogo, «falta rustc» con `rust`, y
«falta neomutt» con `mutt`. El parentesco se acotó a PREFIJO o SUFIJO después de ver que «contenido
en cualquier posición» daba `age` como pariente de `virt-manager` y `patch` de `speech-dispatcher`:
un pariente falso manda a revisar lo que no hay que revisar, aunque sea menos grave que un hueco
falso. Quedan 3 a revisar, los tres legítimos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

138e6e2524001b8f62700ec0880b6d05657cc62c · por Sergio · (hace 6 d) · padre 1281bb60d · ver el árbol en este commit

Delta semántico · 3

scripts/censo-distro.py

2 archivos · +598 −0

diff --git a/docs/state/distro-huecos.json b/docs/state/distro-huecos.json
new file mode 100644
index 00000000..4cf67124
--- /dev/null
+++ b/docs/state/distro-huecos.json
@@ -0,0 +1,447 @@
+{
+ "categorias": [
+  {
+   "categoria": "escritura del mundo",
+   "faltan": [
+    "noto-fonts-cjk",
+    "noto-fonts-emoji",
+    "noto-fonts",
+    "noto-fonts-extra"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "noto-fonts-cjk",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "noto-fonts-emoji",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "noto-fonts",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "noto-fonts-extra",
+     "parientes": []
+    }
+   ],
+   "porque": "sin estas fuentes la distro muestra cuadraditos en chino, japonés, coreano, árabe, hindi, tailandés y emoji — o sea para la mayor parte de la humanidad"
+  },
+  {
+   "categoria": "escribir en esos idiomas",
+   "faltan": [
+    "fcitx5",
+    "anthy",
+    "libpinyin",
+    "hangul",
+    "rime"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "fcitx5",
+     "parientes": []
+    },
+    {
+     "estado": "tiene-y-viaja",
+     "nombre": "ibus",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "anthy",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "libpinyin",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "hangul",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "rime",
+     "parientes": []
+    }
+   ],
+   "porque": "una fuente deja LEER; para ESCRIBIR hace falta un método de entrada"
+  },
+  {
+   "categoria": "accesibilidad",
+   "faltan": [
+    "orca",
+    "speech-dispatcher",
+    "espeak-ng",
+    "brltty"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "orca",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "speech-dispatcher",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "espeak-ng",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "brltty",
+     "parientes": []
+    }
+   ],
+   "porque": "sin lector de pantalla ni voz la distro es inusable para quien no ve"
+  },
+  {
+   "categoria": "imprimir de verdad",
+   "faltan": [
+    "ghostscript",
+    "gutenprint",
+    "hplip"
+   ],
+   "items": [
+    {
+     "estado": "tiene-y-viaja",
+     "nombre": "cups",
+     "parientes": []
+    },
+    {
+     "estado": "revisar",
+     "nombre": "cups-filters",
+     "parientes": [
+      "cups"
+     ]
+    },
+    {
+     "estado": "falta",
+     "nombre": "ghostscript",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "gutenprint",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "hplip",
+     "parientes": []
+    }
+   ],
+   "porque": "cups solo no imprime: los filtros y los drivers son otro paquete"
+  },
+  {
+   "categoria": "escanear",
+   "faltan": [
+    "sane-backends"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "sane-backends",
+     "parientes": []
+    },
+    {
+     "estado": "revisar",
+     "nombre": "simple-scan",
+     "parientes": [
+      "simple-http-server"
+     ]
+    }
+   ],
+   "porque": "ningún camino hoy"
+  },
+  {
+   "categoria": "java",
+   "faltan": [
+    "openjdk",
+    "jdk",
+    "temurin"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "openjdk",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "jdk",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "temurin",
+     "parientes": []
+    }
+   ],
+   "porque": "un ecosistema entero queda fuera sin una JVM"
+  },
+  {
+   "categoria": "documentos técnicos",
+   "faltan": [
+    "texlive",
+    "pandoc"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "texlive",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "pandoc",
+     "parientes": []
+    },
+    {
+     "estado": "tiene-sin-viajar",
+     "nombre": "typst",
+     "parientes": []
+    }
+   ],
+   "porque": "nada que componga un PDF desde texto marcado"
+  },
+  {
+   "categoria": "bases de datos de servidor",
+   "faltan": [
+    "postgresql",
+    "mariadb",
+    "redis"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "postgresql",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "mariadb",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "redis",
+     "parientes": []
+    },
+    {
+     "estado": "tiene-sin-viajar",
+     "nombre": "valkey",
+     "parientes": []
+    }
+   ],
+   "porque": "sqlite está; un servicio con varios clientes no"
+  },
+  {
+   "categoria": "contenedores",
+   "faltan": [
+    "podman",
+    "docker",
+    "buildah",
+    "flatpak"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "podman",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "docker",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "buildah",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "flatpak",
+     "parientes": []
+    }
+   ],
+   "porque": "`crun` está (el motor); falta quien lo conduzca"
+  },
+  {
+   "categoria": "virtualización",
+   "faltan": [
+    "qemu",
+    "libvirt",
+    "virt-manager"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "qemu",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "libvirt",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "virt-manager",
+     "parientes": []
+    }
+   ],
+   "porque": "ni invitado ni anfitrión"
+  },
+  {
+   "categoria": "archivos comprimidos ajenos",
+   "faltan": [
+    "unzip",
+    "zip",
+    "p7zip",
+    "7zip",
+    "unrar"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "unzip",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "zip",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "p7zip",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "7zip",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "unrar",
+     "parientes": []
+    }
+   ],
+   "porque": "no se puede abrir un .zip, un .7z ni un .rar — un adjunto cualquiera queda cerrado"
+  },
+  {
+   "categoria": "vpn y red gestionada",
+   "faltan": [
+    "openvpn",
+    "iwd"
+   ],
+   "items": [
+    {
+     "estado": "tiene-y-viaja",
+     "nombre": "networkmanager",
+     "parientes": []
+    },
+    {
+     "estado": "tiene-y-viaja",
+     "nombre": "wireguard-tools",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "openvpn",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "iwd",
+     "parientes": []
+    }
+   ],
+   "porque": "networkmanager y wireguard están; el resto no"
+  },
+  {
+   "categoria": "juegos",
+   "faltan": [
+    "wine",
+    "gamescope",
+    "lutris"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "wine",
+     "parientes": []
+    },
+    {
+     "estado": "revisar",
+     "nombre": "steam",
+     "parientes": [
+      "steam-runtime-sniper",
+      "steampipe"
+     ]
+    },
+    {
+     "estado": "falta",
+     "nombre": "gamescope",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "lutris",
+     "parientes": []
+    }
+   ],
+   "porque": "decisión abierta, no un olvido"
+  },
+  {
+   "categoria": "discos y salud del equipo",
+   "faltan": [
+    "smartmontools",
+    "nvme-cli",
+    "hdparm"
+   ],
+   "items": [
+    {
+     "estado": "falta",
+     "nombre": "smartmontools",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "nvme-cli",
+     "parientes": []
+    },
+    {
+     "estado": "falta",
+     "nombre": "hdparm",
+     "parientes": []
+    }
+   ],
+   "porque": "nada que avise de un disco muriéndose"
+  }
+ ],
+ "en_alguna_imagen": 592,
+ "recetas_alcanzables": 1375,
+ "resumen": {
+  "falta": 45,
+  "revisar": 3,
+  "tiene-sin-viajar": 2,
+  "tiene-y-viaja": 4
+ },
+ "schema": "takana-censo-distro/1"
+}
diff --git a/scripts/censo-distro.py b/scripts/censo-distro.py
new file mode 100755
index 00000000..a766584d
--- /dev/null
+++ b/scripts/censo-distro.py
@@ -0,0 +1,151 @@
+#!/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.
+
+⚠ **Y no cuenta un hueco sin antes buscar un PARIENTE de nombre.** Es el error que este censo cometió en su
+primera versión y que ya había costado dos veces el mismo día: decir «falta `clang`» cuando existe
+`clang18`, «falta `rustc`» cuando existe `rust`, «falta `neomutt`» cuando existe `mutt`. Un hueco
+con un pariente cerca sale como `revisar`, nunca como `falta` — porque la diferencia entre las dos
+palabras es la que hace que alguien trabaje de más.
+
+Uso:  censo-distro.py [--json docs/state/distro-huecos.json]
+"""
+import argparse
+import glob
+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 = {
+    'escritura del mundo': (
+        'sin estas fuentes la distro muestra cuadraditos en chino, japonés, coreano, árabe, hindi, '
+        'tailandés y emoji — o sea para la mayor parte de la humanidad',
+        ['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',
+        ['fcitx5', 'ibus', 'anthy', 'libpinyin', 'hangul', 'rime']),
+    'accesibilidad': (
+        'sin lector de pantalla ni voz la distro es inusable para quien no ve',
+        ['orca', 'speech-dispatcher', 'espeak-ng', 'brltty']),
+    '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']),
+    'java': ('un ecosistema entero queda fuera sin una JVM', ['openjdk', 'jdk', 'temurin']),
+    'documentos técnicos': ('nada que componga un PDF desde texto marcado',
+                            ['texlive', 'pandoc', 'typst']),
+    'bases de datos de servidor': ('sqlite está; un servicio con varios clientes no',
+                                   ['postgresql', 'mariadb', 'redis', 'valkey']),
+    'contenedores': ('`crun` está (el motor); falta quien lo conduzca',
+                     ['podman', 'docker', 'buildah', 'flatpak']),
+    'virtualización': ('ni invitado ni anfitrión', ['qemu', 'libvirt', 'virt-manager']),
+    'archivos comprimidos ajenos': ('no se puede abrir un .zip, un .7z ni un .rar — un adjunto '
+                                    'cualquiera queda cerrado',
+                                    ['unzip', 'zip', 'p7zip', '7zip', 'unrar']),
+    'vpn y red gestionada': ('networkmanager y wireguard están; el resto no',
+                             ['networkmanager', 'wireguard-tools', 'openvpn', 'iwd']),
+    'juegos': ('decisión abierta, no un olvido', ['wine', 'steam', 'gamescope', 'lutris']),
+    'discos y salud del equipo': ('nada que avise de un disco muriéndose',
+                                  ['smartmontools', 'nvme-cli', 'hdparm']),
+}
+
+
+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 si viaja) · `revisar` (hay un pariente) · `falta` (ni por subcadena)."""
+    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 ('revisar' if cerca else 'falta'), cerca[:4]
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--json')
+    a = ap.parse_args()
+
+    import yupana
+    conocidos = nombres_conocidos()
+    en_imagen = {n for (c, n) in yupana.membresia_imagen() if c != 'wanted'}
+
+    filas, resumen = [], {'tiene-y-viaja': 0, 'tiene-sin-viajar': 0, 'revisar': 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']})
+
+    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'))
+    rev = [(i['nombre'], i['parientes']) for f in filas for i in f['items'] if i['estado'] == 'revisar']
+    if rev:
+        print('   — a REVISAR (hay un pariente, no se cuentan como hueco):')
+        for n, p in rev:
+            print('        %-20s → %s' % (n, ', '.join(p)))
+    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())