metis-desk/docs/architecture/resources-global-scope/05-contrato-persistencia.md
2026-10-25 01:19:17 +02:00

18 KiB
Raw Blame History

R2.0 — Contrato conceptual de persistencia

Ratificación posterior — 24 de octubre de 2026: D01–D10 y D04-R ratificadas en los términos recomendados de R2.0; variante A, Resource único con scope explícito. Solo se autoriza implementar R2.1a. El registro vigente de alcance y evidencias está en 11 — Lectores compatibles. Los estados «propuesto», «recomendado» y «pendiente» del cuerpo se conservan como evidencia de la versión documental inicial y quedan actualizados por este registro; ratificación conceptual no equivale a ejecución de R2.1b–R2.6.

Estado: RECOMENDADA D01/D10. No es una migración ejecutable ni autoriza DDL. Se preservan MariaDB, Sequelize v6, normalización y migraciones explícitas, sin sync/alter automático.

1. Motor y compatibilidad comprobada

compose.yaml configura mariadb:11.8; la auditoría visual también fija una imagen 11.8 por digest en scripts/visual-audit.py. La configuración del servidor usa dialect mariadb. Esto confirma la línea del proyecto, no versión patch, modos, engine/collation o esquema de una instancia real no consultada. compose.yaml:4 database.ts:15.

MariaDB soporta CHECK con evaluación en INSERT/UPDATE; la variable check_constraint_checks permite deshabilitarlo y debe estar ON en la sesión de migración y pruebas. El soporte existe desde 10.2.1, anterior a 11.8: es compatible a nivel documental, sin prometer que el DDL exacto ya pasó en esa instalación. Referencia oficial CHECK, referencia oficial de variables, Knowledge Base oficial, soporte desde 10.2.1.

Antes de R2.1b: consultar VERSION(), SHOW CREATE TABLE, information_schema de tablas/columnas/FKs/índices y variables check_constraint_checks/foreign_key_checks en metis_desk_test. Ensayar contra 11.8 configurada y registrar patch/modos/collation reales. No aceptar un test con CHECK desactivado como validación. No conectar BD real para R2.0.

2. Esquema conceptual de Resource

Solo resources añade ámbito; companyId mantiene tipo UUID físico, FK a companies y acciones actuales. No cambiar las tablas de instalación/configuración/licencia a compañía nullable. No usar JSON para producto o historial.

-- Definición conceptual final del nuevo atributo, no un ALTER ejecutable.
scope VARCHAR(7) CHARACTER SET ascii COLLATE ascii_bin NOT NULL

-- Nombre propuesto de constraint; no existe hoy.
CONSTRAINT resource_scope_owner CHECK (
  (scope = 'global' AND companyId IS NULL)
  OR
  (scope = 'company' AND companyId IS NOT NULL)
)

El NOT NULL del discriminador evita que un CHECK de resultado UNKNOWN tolere scope ausente. Comparación binaria del atributo nuevo evita aceptar GLOBAL/Company por collation case-insensitive. No se cambia la collation de UUID/nombres existentes. Dominio/HTTP usan literales exactos; no normalizan un scope desconocido silenciosamente.

PROPUESTO: CHECK adicional de global sin integración: para scope global, integrationKind y todos sus campos de cuenta/mailbox/access son NULL (addressFieldId, contextFieldId, mailboxKind, accountId, referenceFieldId, resourceTypeId, systemId, environmentId). No debe existir fila global en proyecciones SMB/directorio/asistencia. El CHECK de raíz no puede validar la plantilla/campos en otra tabla ni impedir por sí solo todas las filas de proyección: Resources valida el catálogo en Application y conserva las FKs operativas compuestas.

templateId sigue FK a resource_definitions. Elegibilidad exige kind template, stockKey template:application y owner global con campos globales, validado con catálogo bajo lock. No añadir IDs fijos al DDL ni enums dependientes del nombre de plantilla. No se añaden tablas de ApplicationCatalog, adopción, permisos, fabricante o idempotencia en R2.1.

scope/owner son inmutables por Application/dominio; el CHECK verifica una fila válida, no compara OLD/NEW ni impide una conversión directa por un operador SQL privilegiado. No se inventa un trigger de inmutabilidad. Las credenciales del proceso y los procedimientos de migración siguen siendo frontera de confianza; ninguna API pública expone ese UPDATE.

3. FKs e índices que se conservan

Restricción real Garantía Contrato con nullable en padre
resources.companyId → companies.id RESTRICT Dueño local existente Se conserva; NULL permitido solo en raíz global por CHECK
resources.templateId → resource_definitions.id Definición existente Se conserva; no demuestra elegibilidad global por sí sola
directory_resource_scope_template unique(id,companyId,templateId) Clave compuesta para proyecciones Se conserva mismo orden, nunca se sustituye por scope solo
resource_company_identity unique(companyId,id) Clave para asistencia local Se conserva; hijo tiene companyId NOT NULL, no puede referenciar global
directory_resource FK(resourceId,companyId,templateId) Dominio/recurso/compañía/plantilla coinciden Se conserva con hijo NOT NULL y semántica windows_domain
network_share_resource FK(resourceId,companyId,templateId) Carpeta local y plantilla correcta Se conserva con hijo NOT NULL y shared_folder
support_resource_company FK(companyId,resourceId) Asistencia actual local Se conserva; resourceId histórico sigue FK simple
software resourceId y licenseResourceId → resources.id, actual/histórico Existencia y conservación de referencia Sin cambios; propósito/compañía no se deduce de FK simple
application/certificate resourceId → resources.id, actual/histórico Referencia estable Sin cambios; solo application puede aceptar global vía policy
remote targetResourceId → resources.id, actual/histórico Referencia de destino Sin cambios; policy/lector remoto siguen solo locales
resource_revision_position_unique(resourceId,position) y FK a users/recurso Orden/actor/padre Se conserva
FKs de campos/especificaciones/revisiones Capturas normalizadas Se conservan; scope/owner no se repiten en cada hijo

Evidencia: resource.model.ts:19 directory-domain-migration.ts:5 network-share-migration.ts:4 support-migration.ts:86 software-installation-snapshot.ts:7 configuration-snapshots.ts:57 configuration-snapshots.ts:80 mail-remote-snapshots.ts:83 resource-revision-specifications.ts:39.

Una FK nullable en un hijo pierde verificación de coincidencia cuando alguno de sus componentes es NULL; por eso no se hace nullable companyId operativo para «permitir» globales. UNIQUE con NULL no garantiza nombre global único. No añadir unique(scope,companyId,name), ni companyId ficticia para sortear NULL. MariaDB FK, MariaDB ALTER TABLE/UNIQUE.

Si ALTER companyId está bloqueado por FK dependiente, no ejecutar changeColumn a ciegas: capturar definición completa, determinar orden exacto de retirada/restauración en ensayo, pausar escrituras de todas las tablas afectadas, modificar y restaurar las mismas garantías antes de reabrir. No foreign_key_checks=0. Puede mantenerse la FK sin retirarla si el motor admite el cambio ensayado; el contrato no promete esa capacidad sin prueba. Error oficial 1832.

4. Índices y consulta mixta

Preservar PK id y companyId,id; scope solo no identifica ni autoriza. Índice nuevo recomendado inicial: (scope,active,templateId,id) para rama global activa. No se declara imprescindible ni óptimo sin EXPLAIN; R2.1 ensaya carga y decide incluirlo o documentar que otro índice existente satisface el contrato. Para catálogo sin active, templateId/scope pueden tener distribución distinta; medir antes de sumar combinaciones. Índice local adicional (companyId,templateId,active,id) es candidato, no cambio automático.

Candidatos: dos ramas disjuntas global elegible y local companyId=C, UNION ALL de metadatos autorizados; propósito/compañía/ámbito/actividad se filtran en SQL. Búsqueda normalizada, orden y cursor se aplican después de la unión en el servicio de servidor y antes de paginar, según 04; no usar LIMIT anticipado ni afirmar que una collation genérica equivale al helper local. Ambas ramas resuelven clasificación con definitions, no campo duplicado de usage en Resource. Por ID/batch usar PK con policy y proyección mínima. No ejecutar inventario completo+historia por resultado del selector ni consulta de nombre por cada fila.

Buscar substring sobre nombre/provider normalizado no queda acelerado necesariamente por B-tree. La estrategia inicial no añade columnas derivadas y recorre solo proyección mínima de metadatos, con coste a medir; una optimización de proyección indexada requiere aprobación propia y paridad de búsqueda. No crear FULLTEXT ni índices sobre todo TEXT sin necesidad medida. Sin EXPLAIN/volumen en R2.0, evaluación teórica. La política de homónimos D07 no introduce índices únicos de nombre global.

5. Dominio, mapper y Sequelize

No existe ResourceModel extends Model con belongsTo para inventario: migrateInventory crea tablas y el adaptador reconstruye por SQL. Sí hay modelos de catálogo. Los cambios futuros deben respetar esa arquitectura, sin inventar asociaciones ORM para solucionar alcance. resource.model.ts:19 resource-inventory-relational.ts:195 resource-catalog.model.ts:48.

Mapper nuevo distingue par scope/owner estricto; ausencia de scope solo es válida en modo de esquema legacy detectado por adaptador transitorio R2.1a. Esquema migrado con scope NULL/desconocido es corrupción, no default company. Resource.create valida invariantes nuevas; rehydrate conserva UUID/historia y reconstruye Value Objects sin generar identidades. TemplateId y datos de fila se contrastan como hoy.

Historia sigue en resource_revisions y tablas de especificación/valores históricas; scope/owner se heredan de raíz inmutable. Backfill no inserta revisiones de negocio ni actualiza recordedAt; no cambia nombre, características, actor ni UUID. Globales nuevos tienen historia desde creación y solo de sus propios cambios. Datos legacy de Resource local siguen legibles sin snapshots de scope retroactivos.

Propagación debe separar recursos globales de compañías, manteniendo snapshot de cada revisión y campos retirados con valor. No pasar NULL a UniqueID.create ni a captureResourceSpecification que aún presupone compañía. La rama global valida plantilla/campos globales, no usa una compañía temporal. Cambios a plantilla/campos y todos los Resource derivados se guardan en una única transacción DML o se revierten enteros.

6. Unidad de consistencia y concurrencia

Problema confirmado: repositorios de instalación/perfil bloquean compañía pero resources.list abre transacción distinta. Eso no garantiza actividad/elegibilidad al commit. sequelize-software-installation.repository.ts:27 sequelize-application-configuration.repository.ts:34 resource-inventory.service.ts:7.

Contrato recomendado conservador: usar la raíz global de ResourceCatalog existente como punto de serialización para mutaciones de producto/plantilla y escrituras de instalación/perfil que validan sus referencias. Misma transacción Sequelize, sin consultar otro snapshot por un get independiente.

Orden total para caminos afectados: (1) raíz de catálogo Resources, (2) catálogo global Access si procede, (3) compañías por UUID ascendente, (4) workspace Access local si procede, (5) raíces Resource por UUID ascendente, (6) raíces operativas del consumidor. Una operación toma solo los locks que necesita, nunca uno anterior después de uno posterior. La propagación bloquea todas las compañías necesarias en orden antes de recursos. Global status/alta no necesita bloquear compañías solo para actualizar su raíz. Los caminos de borrado local y proyecciones se revisan para conservar el orden existente catálogo→Access→compañía y no invertirlo.

Se propone factoría pública de servicio ligado a transacción desde composition root Resources, siguiendo lectores públicos de directorio/SMB existentes. Solo Infrastructure de Companies aporta Transaction y consume el puerto; Application/Domain no importan Sequelize. El servicio no abre otra transacción ni hace commit propio, no exporta modelos/repositorio y no invoca callback con código no validado como sustituto de unidad de trabajo. Nombres concretos del puerto/factoría se elegirán al implementar siguiendo esta responsabilidad, no se afirman existentes. module.ts:39.

Semántica de carrera: si asociación adquiere punto de serialización primero, valida activo y puede commit; una desactivación posterior vuelve needsReview true en próxima lectura. Si desactivación commit primero, alta/reconfiguración dependiente posterior falla por no disponible. Edición de notas/retirada sigue su policy. Borrado futuro contra relación nueva conserva protección FK y no puede borrar un recurso referenciado. Estos son resultados observables de T15, no timing arbitrario.

Coste explícito: serializar por catálogo reduce concurrencia entre compañías. Es solución mínima segura apoyada en locks ya existentes, sin promesa de rendimiento. R2.2 mide contención y, si resulta inaceptable, se requiere ADR de locking por recurso/plantilla con mismas garantías; no retirar locks por intuición. El flujo actual de otros módulos se verifica antes de activar para no introducir lock order inverso.

7. Backfill, adopción de esquema y rollback

R2.1a detecta capacidades del esquema durante arranque mediante lectura controlada, no describeTable en cada petición ni captura genérica que oculte errores. Soporta legacy intacto y esquema expandido completo; estado parcial solo permitido al proceso de migración, el servidor falla cerrado con diagnóstico saneado. La versión de esquema no se deduce de que una query haya fallado.

R2.1b: captura/restauración ensayadas, pausa escrituras afectadas, añadir scope transitorio default company para compatibilidad de escritores sin scope, rellenar filas anteriores, verificar que siguen con dueño válido, introducir CHECK de combinación/no integración global, permitir NULL en raíz manteniendo FK y crear índices justificados. Mientras convivan writers legacy solo producen company con default y ningún global está habilitado. Tras retirar writers legacy, el schema final puede mantener default company exclusivamente para compatibilidad de migraciones internas existentes; los nuevos comandos globales lo envían explícito. No dejar default que pueda convertir un fallo de scope global en un local sin compañía: CHECK lo impediría.

No scope en software/application ni alteración de licencia/revisiones. Tras cada paso verificar recuentos, checks, FKs y ausencia de referencias cruzadas. DDL puede hacer commit implícito y tomar metadata locks: usar pasos idempotentes/reanudables y plan de recuperación ensayado, no asumir rollback SQL de toda la migración. MariaDB implicit commit.

Antes de primer global: posible rollback de aplicación a legacy si datos se mantienen locales, esquema es compatible y checks/defaults validados; revertir nullable solo si cero filas globales/NULL, con mismo ensayo de FKs. Tras primer global: rollback normal solo a lectores que lo soporten, aunque no tenga usuarios; no purgarlo para bajar versión. Tras referencia global: también todas las superficies operativas necesitan lectores compatibles. Detener flags de alta/uso no borra datos ni desactiva resolución de existentes. Restauración de backup requiere conjunto completo y aceptación explícita de cambios posteriores, no copia parcial de tabla ni empresa ficticia. Secuencia y gates en 07.