ADR-059: Vigencia de las versiones de la TRD/TVD sin solape¶
Estado: Aceptado
Fecha: 2026-10-30
Autores: Giampiero (mantenedor principal); épica «vigencia sin solape», salda la deuda declarada en ADR-058 (2.º punto)
Relacionados: ADR-025 (TRD versionada), ADR-054 (cuadro público e histórico), ADR-058 (gate de formato), ADR-044 (search_path)
Contexto¶
Al convalidar la versión N+1, la N quedaba derogada con valid_to = CURRENT_DATE: el día en que se ejecutó la
derogación (en la zona de la sesión de PostgreSQL, UTC), no el del acto. Como la N+1 se convalida casi siempre con una
fecha_convalidacion retroactiva (el acto llega antes de que alguien lo registre), las dos versiones cubrían
[fecha_convalidacion(N+1), hoy). Además:
fecha_convalidacionno tenía ningún límite: se aceptaba una fecha futura y una N+1 anterior o igual a la de la N.- Dos remedios de lectura gemelos escondían el defecto: document (
_una_version_por_serie, el gate de formato) se quedaba con la devigente_desdemayor, y archive (_HASTA, cuadro público e histórico) tomaba el mínimo entrevalid_toy la convalidación de la siguiente. Cada lector decidía por su cuenta. - El CHECK
trd_series_vigencia_check(037) exigíavalid_to >= valid_from, perovalid_fromes la fecha de registro de la fila: unamigradacargada hoy y relevada por una N+1 convalidada en marzo reventaba en cuantovalid_topasara a ser la fecha del acto. reject_trd_version_mutation(082) no protegíavalid_toni elestadode unaderogada: el motor dejaba reescribir el fin de vigencia y «resucitar» una versión derogada.- Las fechas del día eran
datetime.now(UTC).date()en archive y la zona de radicación (America/Bogota) en document.
Decisión¶
D1. Vigencia semiabierta [desde, valid_to); valid_to(N) = fecha_convalidacion(N+1)¶
valid_to pasa a significar el primer día en que la versión ya no rige (exclusivo). Todos los lectores ya comparaban
valid_to > d, así que N y N+1 quedan contiguas, sin hueco ni solape. Una derogación el mismo día de la convalidación da el
rango vacío [d, d) (nunca rigió), que es verdad y no rompe nada. Alternativa descartada: valid_to = fecha(N+1) − 1
(inclusivo): obliga a reescribir todos los lectores a >=, deja un día de hueco con cualquiera que siga en > y no tiene
representación para «derogada el mismo día».
desde es la fecha_convalidacion; en una migrada (sin acto) el sistema solo conoce valid_from (registro) y, si hay
certificado RUSD con rusd_convalidacion_declarada, la fecha del acto que se ratificó. valid_from no cambia: se documenta
como «fecha de registro de la versión», no como inicio de vigencia.
D2. El servicio escribe valid_to; el motor garantiza lo que no depende del reloj¶
| Acto | valid_to |
Quién |
|---|---|---|
Convalidar N+1 desde aprobada (TRD y TVD) |
fecha_convalidacion de la N+1, en el mismo UPDATE que pone estado='derogada' |
servicio (parámetro, no CURRENT_DATE) |
Ratificar migrada → convalidada |
ninguno (la misma fila sigue vigente) | — |
| Derogar sin sucesora | fecha_acto del acto de derogación (cuerpo obligatorio) |
servicio (parámetro) |
Sobre una derogada |
nada, nunca | motor |
Se descartó fijarlo en un trigger AFTER UPDATE de la N+1: movería una fila distinta desde un trigger (efecto lateral
invisible al asiento) y obligaría a promover la N+1 antes de derogar la N, violando ux_trd_series_vigente.
Lock-then-check. Dentro de la transacción, SELECT … FOR UPDATE de la vigente (trd_series; v_tvd_agrupaciones para
TVD), comprobación del orden sobre la fila bloqueada y UPDATE … WHERE id = $1 AND valid_to IS NULL por id. Sin el
bloqueo, una ratificación concurrente de la migrada le pone una fecha_convalidacion que el chequeo no vio y el CHECK del
motor reventaba con un 500.
D3. Casos¶
migradarelevada:estado='derogada',valid_to = fecha_convalidacion(N+1),fecha_convalidacionsigue NULL. Puede quedarvalid_to < valid_from(la N+1 se convalidó antes de que lamigradase cargara): lamigradanunca rigió en el sistema, y document no la devuelve. Si su certificado declara la fecha del acto que la convalidó, la N+1 debe ser posterior; sin certificado no hay cota (no se inventa una convalid_from).migradaratificada: fecha no futura;<= rusd_certificado_fecha; y, si hayrusd_convalidacion_declarada, igual a ella: el certificado declara la fecha del acto que se ratifica, y dos fechas del mismo acto son dos inicios de vigencia.- TVD: comparte
derogar_por_relevo.trd_series_tvd_soporte_checkexime aderogada; comovalid_tose escribe en el mismoUPDATEque el estado, una TVD convalidada antes de la 082 sin soporte (la TVD-T12 detenant_demo) se puede relevar o derogar. Partirlo en dosUPDATElo prohíbe el CHECK. - Derogación sin sucesora (ronda de auditoría): lleva su acto (Ac. 001/2024 5.1.4.3/5.1.4.4): cuerpo obligatorio
{acto_administrativo, fecha_acto, instancia},fecha_actono futura y estrictamente posterior al inicio de la vigencia;valid_to = fecha_actoy el acto va al asiento. La baja es definitiva y fija su fin; si luego aparece un acto sustituto anterior a esa fecha no cabe sin solapar (409 trd_vigencia_solapada). La UI lo advierte y pide los tres datos. - Versiones ya
derogada: no se tocan (sin backfill). Suvalid_toviejo (día de ejecución) queda; desde la 083 el motor lo congela. Un par solapado detenant_demopuede devolver dos candidatos en fechas pasadas; es dato de prueba y se acepta.
D4. Reglas de fecha (servicio, zona de la entidad)¶
Un helper único, orpycamcp_common.zona (zona_de(nombre), degrada a UTC con aviso; hoy_en(zona, ahora)), lo usan document
(año del radicado y fecha base, sin gemelo) y archive (hoy la zona de la entidad, ADR-062; antes TZ_RADICACION). Las reglas
viven en un módulo puro, app/core/vigencia.py:
| Regla | Código (status) |
|---|---|
Fecha de acto no futura: fecha_aprobacion_comite, fecha_convalidacion, fecha_publicacion, fecha y fecha_convalidacion_declarada del certificado |
422 fecha_de_acto_futura (+ campo, hoy) |
N+1: fecha_convalidacion > desde(vigente) (estricto) |
422 fecha_convalidacion_no_posterior_a_la_vigente (+ version, vigente_desde) |
| Convalidación no anterior a la aprobación del Comité de la misma versión | 422 fecha_convalidacion_anterior_a_la_aprobacion |
Ratificación con certificado declarado: igual a rusd_convalidacion_declarada |
422 fecha_convalidacion_distinta_de_la_declarada |
fecha_publicacion >= fecha_convalidacion (convalidar y registrar-rusd) |
422 fecha_publicacion_anterior_a_la_convalidacion |
Derogación: fecha_acto no futura y > inicio de la vigencia |
422 fecha_de_acto_futura / 422 fecha_derogacion_no_posterior_a_la_vigencia |
| Respaldo del motor: solape con otra versión del código | 409 trd_vigencia_solapada (+ versión en conflicto) |
Respaldo del motor: valid_to < fecha_convalidacion (solo con datos viejos) |
409 trd_vigencia_inconsistente |
El orden es estricto: igual fecha deja a la N con rango vacío y vuelve a pedir desempate por versión, que es lo que se
retira. No hay CHECK fecha <= CURRENT_DATE en el motor: depende del reloj y de la zona (la sesión es UTC; entre las 19:00 y
las 24:00 de Bogotá sería más laxo que el servicio), no es un invariante de la fila y rompería un dump/restore.
vigente_en_futuro y el día por omisión del cuadro público se miden en la misma zona.
D5. Se retiran los dos remedios de lectura¶
_una_version_por_serie pasa a _la_trd_anula_al_suelto (conserva la regla del tipo suelto y pierde el desempate por versión);
_HASTA se borra y el cuadro público lee valid_to (historial.vigente_hasta = valid_to). Mantenerlos como defensa en
profundidad sería una rama de compatibilidad para un estado que el motor ya no admite, y dos sitios más que repiten la regla.
El guardián es el trigger y sus pruebas. Varias filas siguen siendo legítimas solo cuando el mismo code vive en series
distintas (ambigüedad de código, ADR-058). Esto reemplaza el comentario de _HASTA de ADR-054 y el punto de deuda de
ADR-058.
D6. Migración 083 (tenant)¶
trd_series_vigencia_checkreemplazado (NOT VALID, con intento deVALIDATEy aviso):version >= 1,(estado = 'derogada') = (valid_to IS NOT NULL)yvalid_to >= fecha_convalidacioncuando ambos existen. Salevalid_to >= valid_from.reject_trd_version_mutation: cuerpo íntegro de la 082 +valid_toinmutable una vez escrito +derogadaterminal. El congelado del acto (acto_administrativo,fecha_convalidacion,instancia_convalidante,rusd_radicado,fecha_publicacion) deja de valer solo paraconvalidaday vale también paraderogada;acta_comiteyfecha_aprobacion_comitequedan inmutables enconvalidada,derogaday mientras la versión sigaaprobada(solodevolverlas limpia). Antes unUPDATEdirecto reescribía el acto de una versión ya derogada.assert_trd_vigencia_disjunta()+trg_trd_series_vigencia_disjunta(BEFORE INSERT OR UPDATE OF estado, fecha_convalidacion, valid_to): dos versiones del mismo código e instrumento no rigen el mismo día (exclusion_violation, constrainttrd_series_vigencia_disjunta). La función llevaSET search_path FROM CURRENT(como la 070/077) para resolvertrd_seriesen el schema del tenant.
D7. Expedientes cerrados tras el fin de vigencia¶
Un relevo o una derogación con acto retroactivo deja cerrados, bajo una versión que ya no regía, los expedientes que se
cerraron entre el acto y su registro (Ac. 001/2024 art. 4.4.3 par. 1: el expediente se rige por la TRD vigente al momento
de su cierre; y 5.1.2.7: acto de convalidación; su fecha abre la vigencia, ADR-059). En la misma transacción se marcan
trd_revision_requerida=true con motivo cerrado_tras_fin_de_vigencia (patrón de cerrado_bajo_serie_migrada) los
expedientes de la versión con closed_at en la zona de la entidad >= valid_to, sin pisar otra marca y sin mutar su
instantánea; el conteo va al asiento (expedientes_marcados_revision). No hay vía automática: exige decisión archivística.
Nota (2026-10-30): abrir y reclasificar también miran la vigencia. Como el cierre se rige por la TRD vigente a esa fecha
(Ac. 001/2024 4.4.3 par. 1), abrir bajo una derogada deja el expediente sin instrumento que lo gobierne: el cierre da
409 trd_version_no_vigente y se esquiva trd_derogacion_bloqueada_expedientes_abiertos, que solo se mira al derogar (por
API/MCP; la UI no la ofrece). Ahora todo escritor de expedientes (POST /expedientes, el PATCH que cambia trd_serie_id
y la carga masiva, ítem por ítem) pasa por un solo helper, app/services/amarre_trd.py::amarrar_serie (guardián
test_todo_insert_de_expedientes_pasa_por_el_amarre.py): resuelve la vigente por código (mismo criterio que el cierre:
convalidada/migrada con valid_to IS NULL, TRD y TVD), guarda el id de la vigente (no el que manda el cliente),
valida la plantilla de metadatos de ESA versión y, si no hay, responde 409 trd_serie_no_vigente (solo el código; no se revela
el estado de la pedida). El asiento archive.expediente_created (y el del lote) lleva trd_serie_id (el amarrado, el del
INSERT) y trd_serie_id_solicitado si difiere.
Clasificación provisional (5.1.2.6). Desde la aprobación la entidad «podrá organizar» con la TRD: si el código no tiene
vigente y la fila pedida está aprobada, se amarra ESA fila (clasificacion_provisional: true en la respuesta y en el
asiento). Al convalidar, la misma fila pasa a convalidada: el amarre no se re-apunta. Nunca borrador. El cierre conserva su
409 trd_version_no_vigente mientras no se convalide. devolver (aprobada -> borrador) informa cuántos expedientes abiertos
están amarrados a esa fila (expedientes_abiertos_amarrados, en la respuesta y el asiento) y no los bloquea; el recuento de
derogar es por código y cubre también los amarrados a una aprobada. Esto revisa la decisión inicial de esta nota
(no admitir la aprobada).
Carreras. La lectura va FOR SHARE dentro de la transacción del INSERT/UPDATE (la FK sola solo toma FOR KEY SHARE),
y derogar re-cuenta los abiertos tras tomar su lock (si aparece alguno, 409 y rollback): ninguna orden de llegada deja un
expediente abierto bajo una derogada. Si la apertura esperó el lock de la N mientras un relevo la derogaba y promovía la
N+1, la búsqueda se reintenta una vez en la misma transacción y amarra la N+1 (sin 409 espurio).
D8. Carreras y asientos¶
La ratificación de una migrada lleva en su UPDATE rusd_convalidacion_declarada IS NULL OR = fecha: si entre la lectura
y la escritura se registró un certificado con otra fecha, 409 de carrera. El asiento de una migrada sin certificado
declara vigencia_desde: null y, aparte, registrada_desde: valid_from.
Alternativas para el invariante del motor¶
EXCLUDE USING gist |
Trigger (elegida) | |
|---|---|---|
| Requisitos | extensión btree_gist (nueva en el proyecto) |
ninguno |
| Datos viejos | no admite NOT VALID: el demo con solapes reales falla la migración o queda sin la garantía |
valida solo lo que se escribe desde la 083: el pasado no se reescribe |
| Filas sin acto o invertidas | daterange(lo, hi) con lo > hi lanza error |
comparaciones escalares: rango vacío o invertido no solapa con nada |
| Concurrencia | nativa | ux_trd_series_en_tramite + FOR UPDATE de la vigente |
Consecuencias¶
- Sin backfill: lo ya derogado conserva su
valid_tode ejecución;tenant_demono se recrea. - Contrato: ocho códigos nuevos (422/409),
valid_todocumentado como exclusivo y un cuerpo obligatorio enderogar({acto_administrativo, fecha_acto, instancia}: cambio incompatible, sin rama de compatibilidad). Ningún campo de respuesta nuevo ni retirado. - Los asientos
archive.trd_version_derogada/tvd_…llevanvalid_toyvigencia_desde(yzonaen la derogación explícita); los de convalidación,derogada_valid_to. Los rechazos (422/409) no asientan: no hubo acto. - Frontend:
min/maxen las fechas de acto, aviso de derogación definitiva, vigencia «desde … hasta … (ese día ya no rige)» en el detalle y los ocho códigos con sudetail. MCP: solo descripciones; aprobar, convalidar y derogar siguen fuera.
Deuda declarada¶
- Resuelto (no es deuda): la vigencia rige desde la convalidación (Ac. 001/2024 5.1.2.7), el RUSD habilita efectos (5.1.5.1) y el art. 65 de la Ley 1437/2011 (mod. Ley 2080/2021, art. 15) no aplica a este acto.
- Retroactividad por causal (5.1.5.1, par. 1): no se modela una fecha de efectos distinta de la de convalidación.
- La organización con una
aprobada(5.1.2.6) se admite como clasificación provisional solo si el código no tiene vigente; no hay una vía para listar los expedientes provisionales (se ven pordevolver). - Las actualizaciones sin convalidación que terminan en «concepto» (5.1.4.4, par. 2) no tienen camino propio.
- Un certificado RUSD registrado con una fecha de convalidación declarada errónea no se corrige (es inmutable, ADR-054): el camino es relevar la versión con una nueva. Un camino de corrección con asiento propio sería un acto nuevo.
- Un expediente cerrado tras el fin de vigencia se señala; su resolución (mantener la versión o re-amarrar) quedó resuelta en ADR-061.
- ~~Zona por tenant~~: saldada por ADR-062;
TZ_RADICACIONya no existe. - El stub de
trd_seriesde document-service no reproduce los invariantes de archive (deliberado). - Pares solapados ya existentes en
tenant_demo: quedan como están.