tawasuyu

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 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

⎇ 138e6e2524001b8f62700ec0880b6d05657cc62c scripts/censo-distro.py Python · 151 siq'ikuna · 8.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1#!/usr/bin/env python3
2"""censo-distro.py — qué le falta a la distro para que una persona la use.
3
4── POR QUÉ EXISTE, Y QUÉ PREGUNTA CONTESTA QUE LAS OTRAS NO ────────────────────────────────────
5El repo ya mide tres cosas y ninguna es ésta:
6
7  · `build-state*.json`    ¿lo DECLARADO está construido?        (hoy: 1158/1158, deuda 0)
8  · `frontera-triaje.toml` ¿la clausura de build tiene huecos?   (cerrada, 362/365 decididos)
9  · `cierres.json`         ¿cada perfil cierra?                  (hoy: 10 de 10)
10
11Las tres contestan **«¿está completo lo que ya nombramos?»**. Ninguna contesta **«¿alcanza para
12usarla?»**, y por eso el tablero podía estar entero en verde mientras la distro no sabía mostrar un
13carácter chino. Lo dijo el operador el 2026-10-16: *«no hay qué granjear desde lo que tenemos. pero
14lo que no tenemos, faltan montón de cosas»*. Tenía razón y no existía el número.
15
16⚠ **LA LISTA DE ABAJO ES JUICIO, NO MEDIDA**, igual que la columna `destino` de
17`busybox-applets.tsv` — y conviene que esté dicho en la primera línea del fichero. Lo que SÍ se mide
18es cada renglón: si hay receta (en el catálogo, en una cola o en `tandas/`) y si viaja en alguna
19imagen. Agregar o quitar un renglón es una decisión de producto; el guion sólo dice en qué estado
20está lo que alguien ya decidió que importa.
21
22⚠ **Y no cuenta un hueco sin antes buscar un PARIENTE de nombre.** Es el error que este censo cometió en su
23primera versión y que ya había costado dos veces el mismo día: decir «falta `clang`» cuando existe
24`clang18`, «falta `rustc`» cuando existe `rust`, «falta `neomutt`» cuando existe `mutt`. Un hueco
25con un pariente cerca sale como `revisar`, nunca como `falta` — porque la diferencia entre las dos
26palabras es la que hace que alguien trabaje de más.
27
28Uso:  censo-distro.py [--json docs/state/distro-huecos.json]
29"""
30import argparse
31import glob
32import json
33import os
34import sys
35
36RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
37sys.path.insert(0, os.path.join(RAIZ, 'scripts'))
38
39# Cada entrada: categoría → (por_qué_importa, [nombres candidatos]).
40# El «por qué» no es adorno: un renglón sin motivo se discute para siempre.
41CHECKLIST = {
42    'escritura del mundo': (
43        'sin estas fuentes la distro muestra cuadraditos en chino, japonés, coreano, árabe, hindi, '
44        'tailandés y emoji — o sea para la mayor parte de la humanidad',
45        ['noto-fonts-cjk', 'noto-fonts-emoji', 'noto-fonts', 'noto-fonts-extra']),
46    'escribir en esos idiomas': (
47        'una fuente deja LEER; para ESCRIBIR hace falta un método de entrada',
48        ['fcitx5', 'ibus', 'anthy', 'libpinyin', 'hangul', 'rime']),
49    'accesibilidad': (
50        'sin lector de pantalla ni voz la distro es inusable para quien no ve',
51        ['orca', 'speech-dispatcher', 'espeak-ng', 'brltty']),
52    'imprimir de verdad': (
53        'cups solo no imprime: los filtros y los drivers son otro paquete',
54        ['cups', 'cups-filters', 'ghostscript', 'gutenprint', 'hplip']),
55    'escanear': ('ningún camino hoy', ['sane-backends', 'simple-scan']),
56    'java': ('un ecosistema entero queda fuera sin una JVM', ['openjdk', 'jdk', 'temurin']),
57    'documentos técnicos': ('nada que componga un PDF desde texto marcado',
58                            ['texlive', 'pandoc', 'typst']),
59    'bases de datos de servidor': ('sqlite está; un servicio con varios clientes no',
60                                   ['postgresql', 'mariadb', 'redis', 'valkey']),
61    'contenedores': ('`crun` está (el motor); falta quien lo conduzca',
62                     ['podman', 'docker', 'buildah', 'flatpak']),
63    'virtualización': ('ni invitado ni anfitrión', ['qemu', 'libvirt', 'virt-manager']),
64    'archivos comprimidos ajenos': ('no se puede abrir un .zip, un .7z ni un .rar — un adjunto '
65                                    'cualquiera queda cerrado',
66                                    ['unzip', 'zip', 'p7zip', '7zip', 'unrar']),
67    'vpn y red gestionada': ('networkmanager y wireguard están; el resto no',
68                             ['networkmanager', 'wireguard-tools', 'openvpn', 'iwd']),
69    'juegos': ('decisión abierta, no un olvido', ['wine', 'steam', 'gamescope', 'lutris']),
70    'discos y salud del equipo': ('nada que avise de un disco muriéndose',
71                                  ['smartmontools', 'nvme-cli', 'hdparm']),
72}
73
74
75def nombres_conocidos():
76    """Toda receta alcanzable: catálogo, colas `incoming-*` y `tandas/`."""
77    n = set()
78    for f in glob.glob(os.path.join(RAIZ, 'recipes/*.toml')):
79        n.add(os.path.basename(f)[:-5])
80    for f in glob.glob(os.path.join(RAIZ, 'recipes/incoming-*/*.toml')):
81        n.add(os.path.basename(f)[:-5])
82    for f in glob.glob(os.path.join(RAIZ, 'tandas/**/*.toml'), recursive=True):
83        n.add(os.path.basename(f)[:-5])
84    return n
85
86
87def clasificar(cand, conocidos, en_imagen):
88    """`tiene` (y si viaja) · `revisar` (hay un pariente) · `falta` (ni por subcadena)."""
89    if cand in conocidos:
90        return ('tiene-y-viaja' if cand in en_imagen else 'tiene-sin-viajar'), []
91    raiz = cand.split('-')[0]
92    # Un pariente es: una receta cuyo nombre EMPIEZA por la raíz del candidato (`cups` → `cups`,
93    # `steam` → `steampipe`), o una receta que es PREFIJO o SUFIJO del candidato (`mutt` dentro de
94    # `neomutt`). Y no «contenida en cualquier posición», que es lo que hacía la primera versión:
95    # con eso `age` salía pariente de `virt-manager` y `patch` de `speech-dispatcher`. Un pariente
96    # falso no es inocuo —manda a revisar algo que no hay que revisar— aunque sea menos grave que
97    # un hueco falso. Raíz de 3+ caracteres: con 2 todo se parece a todo.
98    cerca = sorted(x for x in conocidos
99                   if (len(raiz) >= 3 and x.startswith(raiz))
100                   or (len(x) >= 3 and (cand.startswith(x) or cand.endswith(x))))
101    return ('revisar' if cerca else 'falta'), cerca[:4]
102
103
104def main():
105    ap = argparse.ArgumentParser()
106    ap.add_argument('--json')
107    a = ap.parse_args()
108
109    import yupana
110    conocidos = nombres_conocidos()
111    en_imagen = {n for (c, n) in yupana.membresia_imagen() if c != 'wanted'}
112
113    filas, resumen = [], {'tiene-y-viaja': 0, 'tiene-sin-viajar': 0, 'revisar': 0, 'falta': 0}
114    for cat, (porque, cands) in CHECKLIST.items():
115        items = []
116        for c in cands:
117            estado, cerca = clasificar(c, conocidos, en_imagen)
118            resumen[estado] += 1
119            items.append({'nombre': c, 'estado': estado, 'parientes': cerca})
120        filas.append({'categoria': cat, 'porque': porque, 'items': items,
121                      'faltan': [i['nombre'] for i in items if i['estado'] == 'falta']})
122
123    print('== qué le falta a la distro para usarse (checklist de JUICIO, estado MEDIDO) ==')
124    print('   recetas alcanzables: %d · de ellas en alguna imagen: %d'
125          % (len(conocidos), len(en_imagen)))
126    for f in sorted(filas, key=lambda x: -len(x['faltan'])):
127        marca = '✗' if f['faltan'] else '·'
128        print('   %s %-28s %d/%d falta%s' % (marca, f['categoria'], len(f['faltan']),
129                                             len(f['items']),
130                                             ': ' + ', '.join(f['faltan']) if f['faltan'] else 'n'))
131    rev = [(i['nombre'], i['parientes']) for f in filas for i in f['items'] if i['estado'] == 'revisar']
132    if rev:
133        print('   — a REVISAR (hay un pariente, no se cuentan como hueco):')
134        for n, p in rev:
135            print('        %-20s → %s' % (n, ', '.join(p)))
136    print('   resumen: %s' % json.dumps(resumen, ensure_ascii=False))
137
138    if a.json:
139        with open(a.json, 'w', encoding='utf-8') as fh:
140            json.dump({'schema': 'takana-censo-distro/1',
141                       'recetas_alcanzables': len(conocidos),
142                       'en_alguna_imagen': len(en_imagen),
143                       'resumen': resumen, 'categorias': filas},
144                      fh, ensure_ascii=False, indent=1, sort_keys=True)
145            fh.write('\n')
146        print('   → %s' % a.json)
147    return 0
148
149
150if __name__ == '__main__':
151    sys.exit(main())