Apéndice: el esquema, desnudo
Cuando cuentas que le has montado un sistema operativo a tu IA con MySQL, PHP y bash, la primera respuesta honesta de un foro técnico es: enséñalo o no pasó. Es una respuesta justa. Este apéndice es mi contestación.
No voy a publicar mi código ni mis datos — en este blog rige "patrones, no planos", y lo que hay dentro del telar es la operación real de mis clientes. Pero el patrón completo cabe en cuatro tablas, y un esquema no tiene secretos que proteger. Aquí está, saneado de nombres pero fiel en todo lo demás, con el espíritu de los comentarios que lleva en mi SQL. Cualquiera puede reconstruirlo en una tarde. El esquema es la parte fácil; al final hablamos de la difícil.
Los nodos
Todo lo que existe es un nodo de uno de cinco tipos: trabajo (un encargo con ciclo de vida), recurso (un sistema, servidor o cuenta que se usa), procedimiento (un runbook con verificación), persona (quién es, cómo contactarla, qué opera) y regla (un criterio transversal con su comprobación). Una tabla:
CREATE TABLE nodo (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
titulo VARCHAR(200) NOT NULL DEFAULT '',
tipo VARCHAR(15) NOT NULL DEFAULT 'trabajo', -- trabajo/recurso/procedimiento/persona/regla
estado VARCHAR(20) NOT NULL DEFAULT 'pendiente', -- fase (trabajo) / salud (recurso)
estado_previo VARCHAR(20) NOT NULL DEFAULT '', -- para restaurar al desbloquear
prioridad VARCHAR(10) NOT NULL DEFAULT 'normal',
proyecto VARCHAR(40) NOT NULL DEFAULT 'general', -- slug, no FK numérica
fecha_creacion INT UNSIGNED NOT NULL DEFAULT 0,
fecha_cierre INT UNSIGNED NOT NULL DEFAULT 0,
fecha_aviso INT UNSIGNED NOT NULL DEFAULT 0, -- recordatorios integrados
periodicidad_aviso INT UNSIGNED NOT NULL DEFAULT 0, -- días; 0 = aviso único
fecha_modificacion INT UNSIGNED NOT NULL DEFAULT 0, -- epoch: motor del sync
borrado TINYINT(1) NOT NULL DEFAULT 0, -- soft-delete SIEMPRE
PRIMARY KEY (id)
) ENGINE=InnoDB;
Los estados no son adorno: un trabajo solo puede estar en pendiente → abierto → bloqueado → cerrado, y un recurso declara su salud (operativo, degradado, caido, mantenimiento, retirado). La máquina de estados es lo que permite preguntar "¿qué hay vivo?" sin leer nada más.
Las líneas
Cada nodo lleva colgando una bitácora append-only. No se edita el pasado; se añade una línea nueva. Y cada línea está tipada — los tipos son la clave de lectura: encargo (qué se pidió), accion (qué se hizo), decision (por qué se eligió así), resultado (cómo acabó), acceso/uso/estado (la ficha viva de un recurso), verificacion (cómo se comprueba).
CREATE TABLE linea (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
nodo_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
texto TEXT,
tipo VARCHAR(12) NOT NULL DEFAULT 'accion',
autor VARCHAR(20) NOT NULL DEFAULT 'ia', -- ia / humano
fecha_creacion INT UNSIGNED NOT NULL DEFAULT 0,
fecha_modificacion INT UNSIGNED NOT NULL DEFAULT 0,
borrado TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY idx_nodo (nodo_id)
) ENGINE=InnoDB;
La regla de la casa: un trabajo no se cierra sin línea resultado. Esa única obligación convierte la cola de tareas en memoria institucional — meses después, "¿cómo se resolvió aquello?" tiene respuesta literal, con fecha y autor.
Las aristas
El grafo aparece con la tercera tabla. Siete relaciones, cada una con semántica fija:
CREATE TABLE arista (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
origen_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
destino_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
relacion VARCHAR(20) NOT NULL DEFAULT '',
fecha_modificacion INT UNSIGNED NOT NULL DEFAULT 0,
borrado TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY uq_arista (origen_id, relacion, destino_id),
KEY idx_destino (destino_id, relacion)
) ENGINE=InnoDB;
- usa — un trabajo usa un recurso ("este encargo toca ese servidor")
- sigue — un trabajo sigue un procedimiento o una regla (cumplimiento)
- aplica — un procedimiento aplica a un recurso
- contiene — jerarquía: un servidor contiene sus servicios; una organización, sus personas
- bloqueado_por — lo que falta: una persona, o otro trabajo (y al cerrarse el bloqueante, el bloqueado se libera solo)
- documenta — un trabajo documenta los nodos que creó o corrigió (trazabilidad del conocimiento)
- opera — una persona opera una máquina o cuenta
La "matriz de coherencia" que impide enlazar sinsentidos no es magia declarativa: es la lista de combinaciones origen–relación–destino permitidas, validada por el CLI antes de insertar. Un WHERE con opiniones. Con esto, "dame el contexto del encargo 147" es una consulta, no una búsqueda: el encargo, sus recursos con los accesos, sus procedimientos con los pasos y las reglas que le aplican — resueltas transitivamente — en una sola salida.
La bóveda
Cuarta tabla, la única con truco. Los secretos van cifrados por valor (secretbox: nonce + MAC), y la clave no está en la base de datos: se deriva de un PIN que solo sabe el humano más un pepper que vive fuera del repositorio y fuera del sync. Si la tabla se filtra entera, los ciphertexts son ruido.
CREATE TABLE secreto (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
recurso_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
nombre VARCHAR(100) NOT NULL DEFAULT '', -- "SSH root", "API tal"
valor_cifrado TEXT NOT NULL, -- base64(nonce ‖ ciphertext+MAC)
fecha_modificacion INT UNSIGNED NOT NULL DEFAULT 0,
borrado TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY uq_recurso_nombre (recurso_id, nombre)
) ENGINE=InnoDB;
Y el matiz honesto, que ya dejé escrito en el artículo del telar: este diseño minimiza la exposición de los secretos — cifrado en reposo, apertura con PIN humano, recuperación por nombre en el momento de uso. No la elimina. Quien te venda eliminación total, te está vendiendo algo.
El detalle que más me gusta: dos escritores sin servidor central
El telar vive en dos sitios: mi máquina de desarrollo (donde escribe la IA) y producción (donde escribo yo desde la web). Sin colas, sin brokers, sin "tiempo real". El truco entero son tres decisiones:
1. Rangos de id disjuntos: desarrollo arranca su AUTO_INCREMENT en 1; producción, en 1.000.000.000. Los dos insertan sin pisarse jamás.
2. fecha_modificacion en cada tabla y last-write-wins al reconciliar.
3. Prohibido el DELETE físico: el borrado es una columna, y por eso las bajas viajan en el sync como un cambio más.
¿Y si los dos escritores tocan el mismo nodo antes de un sync? La respuesta honesta, por partes. Casi todo el tráfico del telar son INSERT de líneas nuevas — bitácora append-only con rangos disjuntos —, y ahí la colisión es imposible por construcción. El last-write-wins solo muerde en las cabeceras (estado, prioridad, título), donde los dos escritores rara vez coinciden el mismo día sobre el mismo nodo; y cuando ocurra, el pisotón no borra la historia: las líneas de ambos sobreviven, y la bitácora delata la discrepancia a quien la lea. No es un protocolo de consenso; es una superficie de conflicto reducida a propósito hasta donde el riesgo restante se vigila a ojo.
Es la mitad de un CRDT hecho con lo que ya había. Para dos escritores que se fían el uno del otro, sobra. Con diez escritores desconfiados necesitarías relojes vectoriales o un servidor central — y entonces ya no sería este sistema, ni este artículo.
Una regla de verdad, como muestra
Me han pedido un ejemplo real de la consigna "una regla sin verificación es un deseo". Valga esta, que es de las más simples de la casa (abstraída de nombres, fiel en la forma):
nodo: regla #NNN — "Todo deploy termina con smoke test"
linea[nota]: Tras cada despliegue a producción se comprueban
las rutas críticas ANTES de dar el trabajo por cerrado.
linea[verificacion]: curl -s -o /dev/null -w '%{http_code}' https://DOMINIO/
== 200 (repetir para cada ruta crítica del deploy)
arista: procedimiento "desplegar" --sigue--> regla #NNN
La regla no es el texto: es el texto más el comando que la comprueba más la arista que la pone delante de quien despliega. Cuando el guardián arranca un trabajo de deploy, esa verificación aparece en el paquete de contexto. Ejecutarla no es opcional: el trabajo no cierra sin línea de resultado, y el resultado sin smoke test se nota.
Lo que NO está aquí — y es la parte difícil
El esquema se reconstruye en una tarde. Lo que no se instala con un CREATE TABLE:
- Las reglas con verificación. Escribirlas — y podarlas cuando caducan — es trabajo humano.
- La disciplina del resultado. Cerrar sin línea resultado está prohibido. Todos los días. También con prisa.
- La jerarquía de verdad. El telar manda sobre los documentos; la realidad manda sobre el telar; y quien encuentra la discrepancia tiene la obligación de corregirla en el momento.
Mi crítico más duro hasta la fecha — una IA, por cierto — lo resumió mejor que yo: esta disciplina "no se compra instalando nada". Exacto. Por eso publico el esquema sin miedo: las tablas son gratis. Lo que vale es lo otro.
(¿Y la fontanería — cómo habla la IA con todo esto? Sin misterio y sin demonios residentes: está contada en el artículo del telar, sección "La fontanería".)
— un viejo programador · 64 años · rss