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: familia «servicio» — systemd → tarjeta de arje, y el pivote pasa a ser uno POR FAMILIA
1#!/usr/bin/env python3
2"""modelo.py — el MODELO PIVOTE de configuración de servicio, y el acta que lo acompaña (SDD 29).
3
4── POR QUÉ UN PIVOTE Y NO PARES ────────────────────────────────────────────────────────────────
5Traducir de a pares crece en N×M: con nginx, apache, caddy, haproxy y lighttpd son 20 traductores, y
6cada formato nuevo agrega 2N. Con un modelo intermedio son **N lectores + M escritores**: un formato
7nuevo cuesta UNO o DOS plugins, y estrena todos los pares de golpe. Es la diferencia entre un centro
8de traducción y una pila de conversores.
9
10── EL ACTA VIAJA DENTRO DEL MODELO, NO AL COSTADO ──────────────────────────────────────────────
11Ésta es la decisión que hace que el pivote no mienta. Si el acta fuera un efecto de cada traductor,
12lo que un LECTOR no entendió se perdería antes de llegar al escritor. Viajando en el modelo, el
13escritor puede emitirlo como comentario y el humano lo ve en el fichero que va a desplegar.
14
15Doctrina heredada de `soltar`/`paskaq` (tawasuyu): **elisión honesta** — lo que no se pudo traducir
16se reporta con su línea y su motivo, nunca se descarta en silencio.
17"""
18from dataclasses import dataclass, field
19
20
21@dataclass
22class Accion:
23 """Qué hace una ruta. `tipo` ∈ {proxy, archivos, redir, responder, intentar}."""
24 tipo: str
25 valor: str = ""
26 extra: dict = field(default_factory=dict)
27
28
29@dataclass
30class Ruta:
31 """Un camino dentro de un sitio. `clase` ∈ {prefijo, exacto, regex}.
32
33 `regex` se conserva a propósito aunque casi ningún destino lo soporte igual: **perder la
34 distinción sería traducir mal en silencio**. El escritor decide si sabe emitirla o la manda al
35 acta."""
36 patron: str
37 clase: str = "prefijo"
38 acciones: list = field(default_factory=list)
39 origen: int = 0
40
41
42@dataclass
43class Sitio:
44 nombres: list = field(default_factory=list)
45 puertos: list = field(default_factory=list) # "80", "443"
46 tls: dict = field(default_factory=dict) # {cert, key}
47 raiz: str = ""
48 indices: list = field(default_factory=list)
49 rutas: list = field(default_factory=list)
50 acciones: list = field(default_factory=list) # a nivel de sitio
51 origen: int = 0
52
53
54@dataclass
55class Acta:
56 """Registro de procedencia. Cuatro estados, y el que importa es el primero."""
57 entradas: list = field(default_factory=list)
58
59 def anota(self, linea, estado, texto, motivo=""):
60 # estado ∈ {SIN-TRADUCIR, decision, traducido, no-aplica}
61 self.entradas.append({"linea": linea, "estado": estado,
62 "directiva": texto, "motivo": motivo})
63
64 def sin_traducir(self):
65 return [e for e in self.entradas if e["estado"] == "SIN-TRADUCIR"]
66
67 def por_estado(self):
68 d = {}
69 for e in self.entradas:
70 d.setdefault(e["estado"], []).append(e)
71 return d
72
73
74@dataclass
75class Config:
76 """Lo que un lector produce y un escritor consume."""
77 formato_origen: str = ""
78 sitios: list = field(default_factory=list)
79 acta: Acta = field(default_factory=Acta)
80
81
82# ── FAMILIA «servicio» ──────────────────────────────────────────────────────────────────────────
83# ⚠ El pivote NO es uno solo: es uno POR FAMILIA. Un unit de systemd no es un sitio web, y meterlo a
84# la fuerza en `Sitio`/`Ruta` sería justo el «parecerse» que el acta existe para evitar. Cada familia
85# tiene su modelo, y el registro sólo empareja lectores con escritores de la MISMA familia — un par
86# cruzado se rechaza ruidosamente en vez de producir basura plausible.
87
88
89@dataclass
90class Servicio:
91 """Un proceso supervisado. El denominador común de systemd, OpenRC, runit, s6 y arje.
92
93 `usuario` se conserva aunque el destino no siempre sepa emitirlo: es el campo donde una elisión
94 silenciosa **escala privilegios** en vez de sólo perder una opción."""
95 nombre: str = ""
96 descripcion: str = ""
97 tipo: str = "simple" # simple | forking | oneshot | notify | dbus
98 cmd: str = "" # la línea de arranque, tal cual
99 cwd: str = ""
100 usuario: str = ""
101 grupo: str = ""
102 entorno: dict = field(default_factory=dict)
103 entorno_ficheros: list = field(default_factory=list) # no se pueden leer al traducir
104 tras: list = field(default_factory=list) # orden (After=)
105 necesita: list = field(default_factory=list) # dependencia dura (Requires=)
106 reinicio: str = "no" # no | siempre | si-falla
107 reinicio_ms: int = 0
108 arranque_auto: bool = False # ¿lo levanta el arranque? (WantedBy=)
109 plantilla: bool = False # `foo@.service`: no instanciable a ciegas
110 origen: int = 0
111 # {campo: línea} — el lector anota DÓNDE vio cada cosa para que el escritor pueda señalar la
112 # línea real en vez de un 0. Un acta que dice «línea 0» obliga a buscar a mano lo que ya se sabía.
113 lineas: dict = field(default_factory=dict)
114
115
116@dataclass
117class Sistema:
118 """Lo que produce un lector de la familia «servicio» y consume su escritor."""
119 formato_origen: str = ""
120 servicios: list = field(default_factory=list)
121 acta: Acta = field(default_factory=Acta)