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 1173Shell 64Rust 62Markdown 441482 Definiciones

⎇ 534014c3ec69daea789a1d0523e1d00a69c6ed65 scripts/mudanza/traducir.py Python · 156 líneas · 8.0 KB Historia Quién tocó qué crudo

traducir: lector de OpenRC — el par que cierra el camino de gioser, y casi la mitad NO se puede leer 61c33800c · Sergio ·

1#!/usr/bin/env python3
2"""traducir.py — CENTRO de traducción de configuraciones, por modelo pivote y plugins (SDD 29).
3
4Mudar de un servicio a su equivalente no es cambiar un binario: es **reescribir la configuración**.
5Esto hace la parte mecánica y —sobre todo— **dice con número de línea lo que NO pudo traducir**.
6
7── POR QUÉ UN CENTRO Y NO TRADUCTORES DE A PARES ───────────────────────────────────────────────
8De a pares el trabajo crece en **N×M**: con nginx, apache, caddy, haproxy y lighttpd son 20
9traductores, y cada formato nuevo agrega 2N. Con un **modelo pivote** son **N lectores + M
10escritores**: un formato nuevo cuesta uno o dos plugins y estrena todos los pares de golpe.
11
12Probado, no argumentado: el lector de apache se agregó **sin tocar el escritor de Caddy**, y con eso
13`apache → caddy` funciona sin que exista ningún traductor apache→caddy.
14
15── LOS PLUGINS, Y LA FAMILIA ───────────────────────────────────────────────────────────────────
16Un formato se agrega dejando un fichero en `formatos/` con `@lector("x")` y/o `@escritor("y")`. El
17centro los DESCUBRE: no hay una lista que editar, porque una lista que hay que mantener a mano se
18desincroniza y el formato nuevo «no existe» sin que nada falle.
19
20⚠ El pivote es **uno por FAMILIA**, y eso no es taxonomía: un unit de systemd no es un sitio web, y
21un par cruzado produciría basura *plausible*. El centro lo rechaza con un error, no con un intento.
22Hoy hay dos familias — `web` (nginx, apache → caddy) y `servicio` (systemd → arje) — y agregar una
23tercera no toca nada de lo escrito.
24
25── DOCTRINA HEREDADA DE `soltar`/`paskaq` (tawasuyu) ───────────────────────────────────────────
26`soltar` desencadena datos presos en formatos cautivos. Otro dominio, mismos principios:
27**elisión honesta** (lo no traducido se reporta con línea y motivo), **no adivinar en silencio**,
28**procedencia total** y **determinismo**. Y una consecuencia de diseño propia: **el acta viaja DENTRO
29del modelo**, no al costado — si fuera de cada traductor, lo que el LECTOR no entendió se perdería
30antes de llegar al escritor, y el fichero que uno despliega no lo diría.
31
32Uso:
33    scripts/mudanza/traducir.py --list
34    scripts/mudanza/traducir.py --from nginx   --to caddy --in nginx.conf --out Caddyfile
35    scripts/mudanza/traducir.py --from apache  --to caddy --in httpd.conf --out Caddyfile
36    scripts/mudanza/traducir.py --from systemd --to arje  --in /etc/systemd/system --out seed.card.json
37"""
38import argparse, os, sys
39
40sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
41import formatos                                          # noqa: E402
42
43
44def imprimir_acta(acta):
45    por = acta.por_estado()
46    print("\n══ ACTA DE TRADUCCIÓN ══")
47    print(f"   {len(acta.entradas)} directiva(s) leídas\n")
48    titulo = {"SIN-TRADUCIR": "⚠ NO SE PUDO TRADUCIR — hay que escribirlas a mano",
49              "decision": "◆ traducidas con una DECISIÓN — revisalas",
50              "traducido": "✓ traducidas",
51              "no-aplica": "· no aplican en el destino"}
52    for e in ("SIN-TRADUCIR", "decision", "traducido", "no-aplica"):
53        if e not in por:
54            continue
55        print(f"── {titulo[e]} ({len(por[e])}) " + "─" * 22)
56        detalle = e in ("SIN-TRADUCIR", "decision")
57        for a in (por[e] if detalle else por[e][:5]):
58            print(f"   línea {a['linea']:>4}  {a['directiva'][:64]}")
59            if detalle and a["motivo"]:
60                print(f"              └─ {a['motivo'][:98]}")
61        if not detalle and len(por[e]) > 5:
62            print(f"   … y {len(por[e])-5} más")
63        print()
64    n = len(acta.sin_traducir())
65    if n:
66        print(f"   ⚠ {n} sin traducir. Desplegar sin resolverlas es desplegar un servidor INCOMPLETO")
67        print("     que además parece funcionar — el modo de fallo más caro.")
68    return n
69
70
71def _fusionar(a, b):
72    """Junta dos modelos de la MISMA familia: extiende cada campo lista y concatena el acta.
73
74    Genérico a propósito — vale para `Config` y para `Sistema` sin que el centro sepa de ninguno de
75    los dos. Un directorio de units es la entrada NORMAL de systemd, no un caso raro."""
76    import dataclasses
77    if a is None:
78        return b
79    for f in dataclasses.fields(a):
80        va, vb = getattr(a, f.name), getattr(b, f.name)
81        if isinstance(va, list):
82            va.extend(vb)
83    a.acta.entradas.extend(b.acta.entradas)
84    return a
85
86
87def _entradas(ruta):
88    """Un fichero, o todos los `*.service`/`*.conf` de un directorio, en orden determinista."""
89    if os.path.isdir(ruta):
90        # TODOS los ficheros regulares no ocultos, no sólo los de cierta extensión: los servicios de
91        # OpenRC no tienen ninguna. Filtrar acá por nombre sería el centro decidiendo qué es de cada
92        # formato — y lo que no entra se pierde EN SILENCIO. Filtra el lector, que sabe, y lo DICE.
93        hs = sorted(f for f in os.listdir(ruta)
94                    if not f.startswith(".") and os.path.isfile(os.path.join(ruta, f)))
95        if not hs:
96            sys.exit(f"✗ {ruta} no tiene ningún fichero regular")
97        return [os.path.join(ruta, h) for h in hs]
98    return [ruta]
99
100
101def main():
102    lectores, escritores = formatos.cargar()
103    fams = formatos.familias()
104    ap = argparse.ArgumentParser(description="Centro de traducción de configuraciones (pivote + plugins).")
105    ap.add_argument("--list", action="store_true", help="listar formatos disponibles y los pares que habilitan")
106    ap.add_argument("--from", dest="desde", choices=sorted(lectores))
107    ap.add_argument("--to", dest="hacia", choices=sorted(escritores))
108    ap.add_argument("--in", dest="entrada", help="fichero, o directorio (se leen sus *.service/*.conf)")
109    ap.add_argument("--out", dest="salida")
110    a = ap.parse_args()
111
112    if a.list:
113        print("\n══ formatos registrados ══")
114        tot_l, tot_e, pares = 0, 0, 0
115        for fam, (le, es) in fams.items():
116            n, m = len(le), len(es)
117            tot_l, tot_e, pares = tot_l + n, tot_e + m, pares + n * m
118            print(f"\n── familia «{fam}» " + "─" * 40)
119            print(f"   lectores   ({n}): {', '.join(le) or '—'}")
120            print(f"   escritores ({m}): {', '.join(es) or '—'}")
121            for x in le:
122                for y in es:
123                    if x != y:
124                        print(f"     {x} → {y}")
125        print(f"\n   {pares} par(es) habilitados con {tot_l + tot_e} plugin(s).")
126        print(f"   De a pares harían falta {pares} traductores, y cada formato nuevo agregaría "
127              f"tantos como formatos tenga ya su familia.")
128        print("   ⚠ Los pares se cuentan DENTRO de cada familia: un lector de servicios contra un")
129        print("     escritor de web no es un par, es basura plausible. El centro lo rechaza.")
130        return
131
132    if not (a.desde and a.hacia and a.entrada):
133        ap.error("hacen falta --from, --to y --in (o --list)")
134
135    fl, fe = lectores[a.desde][1], escritores[a.hacia][1]
136    if fl != fe:
137        sys.exit(f"✗ `{a.desde}` es de la familia «{fl}» y `{a.hacia}` de la familia «{fe}».\n"
138                 f"  No es un par: traducir entre familias distintas daría un fichero que PARECE\n"
139                 f"  correcto. Mirá `--list` para los pares que sí existen.")
140
141    modelo = None
142    for ruta in _entradas(a.entrada):
143        with open(ruta) as fh:
144            modelo = _fusionar(modelo, lectores[a.desde][0](fh.read(), ruta))
145    texto = escritores[a.hacia][0](modelo)    # el escritor puede AÑADIR al acta: se imprime después
146    n = imprimir_acta(modelo.acta)
147    if a.salida:
148        open(a.salida, "w").write(texto)
149        print(f"\n   {a.hacia} → {a.salida}")
150    else:
151        print(texto)
152    sys.exit(1 if n else 0)
153
154
155if __name__ == "__main__":
156    main()