Traducir de a pares no escala: con nginx, apache, caddy, haproxy y lighttpd son 20 traductores, y cada formato nuevo agrega 2N. Con un modelo intermedio son N lectores + M escritores: un formato nuevo cuesta uno o dos plugins y estrena todos los pares de golpe. **Probado, no argumentado**: el lector de apache se agregó SIN TOCAR el escritor de Caddy, y con eso `apache → caddy` funciona sin que exista ningún traductor de ese par. Las dos salidas las valida el caddy del propio corpus: `Valid configuration` en las dos. Los plugins se DESCUBREN (`formatos/` + `@lector` / `@escritor`): una lista mantenida a mano se desincroniza y el formato nuevo «no existe» sin que nada falle. `--list` dice qué pares habilita. **El acta viaja DENTRO del modelo**, y es la decisión que hace que el pivote no mienta: si fuera un efecto de cada traductor, lo que el LECTOR no entendió se perdería antes de llegar al escritor. El escritor además puede agregarle — Caddy no tiene equivalente de `location ~`, así que en vez de emitir un `handle` de prefijo que SE PARECE, lo manda al acta. Parecerse es peor que faltar. Y un bug del lector de apache, encontrado probando: `ProxyPass` tiene dos formas y dentro de `<Location>` lleva sólo la URL. Mirar sólo la de tres tokens dejaba sin traducir la forma más común y el acta la reportaba como «no reconocida» — un falso negativo que manda a escribir a mano algo que el lector sí sabe hacer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
80 lines
3.2 KiB
Python
80 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
||
"""modelo.py — el MODELO PIVOTE de configuración de servicio, y el acta que lo acompaña (SDD 29).
|
||
|
||
── POR QUÉ UN PIVOTE Y NO PARES ────────────────────────────────────────────────────────────────
|
||
Traducir de a pares crece en N×M: con nginx, apache, caddy, haproxy y lighttpd son 20 traductores, y
|
||
cada formato nuevo agrega 2N. Con un modelo intermedio son **N lectores + M escritores**: un formato
|
||
nuevo cuesta UNO o DOS plugins, y estrena todos los pares de golpe. Es la diferencia entre un centro
|
||
de traducción y una pila de conversores.
|
||
|
||
── EL ACTA VIAJA DENTRO DEL MODELO, NO AL COSTADO ──────────────────────────────────────────────
|
||
Ésta es la decisión que hace que el pivote no mienta. Si el acta fuera un efecto de cada traductor,
|
||
lo que un LECTOR no entendió se perdería antes de llegar al escritor. Viajando en el modelo, el
|
||
escritor puede emitirlo como comentario y el humano lo ve en el fichero que va a desplegar.
|
||
|
||
Doctrina heredada de `soltar`/`paskaq` (tawasuyu): **elisión honesta** — lo que no se pudo traducir
|
||
se reporta con su línea y su motivo, nunca se descarta en silencio.
|
||
"""
|
||
from dataclasses import dataclass, field
|
||
|
||
|
||
@dataclass
|
||
class Accion:
|
||
"""Qué hace una ruta. `tipo` ∈ {proxy, archivos, redir, responder, intentar}."""
|
||
tipo: str
|
||
valor: str = ""
|
||
extra: dict = field(default_factory=dict)
|
||
|
||
|
||
@dataclass
|
||
class Ruta:
|
||
"""Un camino dentro de un sitio. `clase` ∈ {prefijo, exacto, regex}.
|
||
|
||
`regex` se conserva a propósito aunque casi ningún destino lo soporte igual: **perder la
|
||
distinción sería traducir mal en silencio**. El escritor decide si sabe emitirla o la manda al
|
||
acta."""
|
||
patron: str
|
||
clase: str = "prefijo"
|
||
acciones: list = field(default_factory=list)
|
||
origen: int = 0
|
||
|
||
|
||
@dataclass
|
||
class Sitio:
|
||
nombres: list = field(default_factory=list)
|
||
puertos: list = field(default_factory=list) # "80", "443"
|
||
tls: dict = field(default_factory=dict) # {cert, key}
|
||
raiz: str = ""
|
||
indices: list = field(default_factory=list)
|
||
rutas: list = field(default_factory=list)
|
||
acciones: list = field(default_factory=list) # a nivel de sitio
|
||
origen: int = 0
|
||
|
||
|
||
@dataclass
|
||
class Acta:
|
||
"""Registro de procedencia. Cuatro estados, y el que importa es el primero."""
|
||
entradas: list = field(default_factory=list)
|
||
|
||
def anota(self, linea, estado, texto, motivo=""):
|
||
# estado ∈ {SIN-TRADUCIR, decision, traducido, no-aplica}
|
||
self.entradas.append({"linea": linea, "estado": estado,
|
||
"directiva": texto, "motivo": motivo})
|
||
|
||
def sin_traducir(self):
|
||
return [e for e in self.entradas if e["estado"] == "SIN-TRADUCIR"]
|
||
|
||
def por_estado(self):
|
||
d = {}
|
||
for e in self.entradas:
|
||
d.setdefault(e["estado"], []).append(e)
|
||
return d
|
||
|
||
|
||
@dataclass
|
||
class Config:
|
||
"""Lo que un lector produce y un escritor consume."""
|
||
formato_origen: str = ""
|
||
sitios: list = field(default_factory=list)
|
||
acta: Acta = field(default_factory=Acta)
|