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…
traducir: lector de OpenRC — el par que cierra el camino de gioser, y casi la mitad NO se puede leer
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()