ADR-061: Resolución de la revisión por fin de vigencia (cerrado_tras_fin_de_vigencia)¶
Estado: Aceptado
Derogado en parte por ADR-065: destrucción en fases, «punto de no retorno» y causa WORM. Siguen vigentes la resolución de la revisión, la compuerta y la omisión.
Fecha: 2026-10-30
Autores: Giampiero (mantenedor principal); salda la deuda declarada de ADR-059 («se señala, no se reclasifica»)
Relacionados: ADR-025 (amarre inmutable tras el cierre), ADR-044 (search_path), ADR-050 (selección), ADR-053 (criterio ACT del MCP)
Contexto¶
El ADR-059 hizo que la vigencia de una versión de TRD fuera exacta ([convalidación, valid_to)). Con un acto
retroactivo (relevo o derogación) quedan expedientes cerrados entre el acto y su registro bajo una versión que
ya no regía. El sistema los señalaba (trd_revision_requerida, motivo cerrado_tras_fin_de_vigencia) pero la marca
no tenía salida: ninguna acción la limpiaba y el amarre es inmutable tras el cierre.
Normas (texto del Ac. AGN 001/2024, verificado contra el PDF firmado): art. 4.4.3 par. 1 (la transferencia se rige por la TRD vigente al cierre) y par. 2 (se transfiere con el índice electrónico firmado); art. 4.4.2 (cuerpo y parágrafo: no se deben incorporar nuevos documentos a lo transferido); Circular AGN 003/2015 num. 3.2 (el dato histórico no se reescribe). Lo que sigue es decisión de este ADR, no texto de la norma: que la convalidación abra la vigencia (5.1.2.7, ya adoptado en ADR-059); que lo transferido solo se «mantenga» (ancla: el cuerpo del 4.4.2 más la Circular 3.2; el parágrafo no dice «no se modifica» ni «FUID congelado»); y la fórmula «se sustituye con rastro», que es una glosa de la Circular 003/2015 num. 3.2, no una cita.
Decisiones¶
D1. Un acto del Comité, dos salidas, una tabla append-only. expediente_trd_resoluciones (migración 084) guarda
cada resolución con su acta, el snapshot previo y el nuevo; public.audit_log lleva el asiento
archive.trd_revision_resuelta (lo último dentro de la misma transacción). (a) mantener-version limpia la marca
con una causal de catálogo cerrado (sin_version_regente, expediente_transferido, cierre_material_anterior) o la
excepción declarada decision_comite (fundamento de 200 caracteres, el asiento marca excepcion_4_4_3), que solo se
admite si re-amarrar es imposible (hay un impedimento de D4): si reamarre_posible, el Comité re-amarra
(409 causal_incoherente). (b)
reamarrar re-amarra a la versión que regía el día local del cierre y recalcula expedientes.disposition
conservando la base original. Solo closed, solo TRD.
| Elegida: tabla de resoluciones + excepción en el trigger que la exige | Bandera de sesión (SET LOCAL) |
Re-clonar el expediente | |
|---|---|---|---|
| Prueba en el motor | El trigger verifica la fila del acto de la misma transacción (txid_current()), la zona y la vigencia de la destino |
Cualquier código que fije la bandera esquiva el trigger | Rompe el número, el índice firmado y la hoja de ruta |
| Zona | La fila guarda zona y fecha_cierre_local: el motor no depende del reloj ni de la sesión UTC |
Ciega | — |
D2. La versión destino no la elige el cliente. El servidor la resuelve: mismo code, TRD, convalidada|derogada,
fecha_convalidacion <= d < COALESCE(valid_to, infinity) con d el día del cierre en la zona de la entidad. El
cliente envía version_destino, disposicion_destino y retencion_total_destino_anios solo como eco de la vista
previa; si difieren, 409. migrada nunca es destino. Sin regente: solo mantener (sin_version_regente).
D3. WORM: ni se prohíbe (b) ni se inventa un calendario mixto. El snapshot pasa a ser exactamente el de la
destino. Si retiene menos, el candado COMPLIANCE ya puesto no se acorta y queda como piso (la eliminación ya lo
respeta: worm_vigente); la respuesta declara excede_calendario. Si retiene más, se encola en
expediente_worm_extension y lo consume la fase 13 del job de reconciliación (con backoff) tras un intento síncrono
post-commit. Storage es monotónico: reintentar es seguro.
D4. Impedimentos de (b) (409): expediente_transferido, reamarre_solo_trd (TVD), transferencia_en_curso,
expediente_en_lote_eliminacion, disposicion_ya_decidida, seleccion_trd_registrada,
sin_version_regente_al_cierre, version_regente_ambigua.
D5. Mientras la marca esté, el expediente no avanza. 409 trd_revision_pendiente en preparar y recibir una
transferencia (también la vía corta POST /expedientes/{id}/transferir), decidir la disposición vencida y registrar la
selección S/M; la propuesta, el aval y la ejecución de un lote de eliminación lo rechazan (la propuesta con el
motivo trd_revision_pendiente; aval y ejecución, con FOR KEY SHARE sobre todo el inventario antes de tocar nada).
Todo con FOR KEY SHARE sobre la fila (lock-then-check contra el FOR UPDATE del acto; KEY SHARE y no SHARE porque
dos recibir concurrentes con SHARE caían en deadlock al hacer su UPDATE). Además, fail-closed: sobre un
expediente re-amarrado esos mismos actos exigen que el índice vigente tenga worm_protegido_at; si falta,
409 indice_sin_worm. La compuerta devuelve lo que la fila dice bajo el bloqueo y los actos que leyeron el snapshot antes
(decidir, preparar, proponer) lo comparan: si difiere, 409 trd_revision_carrera. El gate de índice firmado de preparar
se evalúa dentro de la transacción (art. 4.4.3 par. 2). El no-read-up de la selección va en el WHERE de la compuerta
(fuera de alcance es 404, no 409).
D6. Permisos. Vista previa: USUA_PERM_TRD nivel 1 y clearance en el WHERE (404 indistinguible). Los dos
POST: nivel 3 (el 4 es de avalar/ejecutar la eliminación), require_actor fail-closed, sin gate de clearance en la
mutación (disposición ⊥ lectura) y respuesta acotada construida desde la fila bloqueada. Los intentos denegados
dejan un asiento best-effort archive.trd_revision_resolucion_fallida.
D7. MCP. Se ofrece la lectura consultar_resolucion_revision_trd; mantener-version y reamarrar no se
ofrecen (motivo ACT: son la constancia de una decisión del Comité tomada fuera del sistema, el mismo criterio que
avalar un lote o convalidar una TRD).
D8. El índice nuevo se protege bajo WORM como una versión encadenada. Storage tenía un único objeto WORM indice por
expediente (índice único parcial de la 009). La migración storage 027 lo relaja a la raíz de la cadena
(renueva_a IS NULL, igual que el AIP en la 023) y añade unicidad por (expediente_id, file_id): la identidad del
objeto pasa a ser (expediente, 'indice', file_id), de modo que la 2.ª versión no responde «ya protegido» sobre la
1.ª, y un file_id ajeno sin declarar nueva_version sigue siendo conflicto. Se descartó un tipo nuevo indice_version:
obligaba a tocar el CHECK de tipos, el listado de renovación y el contrato de archive en tres sitios, y la cascada de
renovación (que desciende por renueva_a desde la raíz) ya da lo que se quiere (la versión vive al menos tanto como el
índice original). La retención de la versión nueva sale de la TRD destino (archive protege con el amarre vivo). El
barrido de la fase 13 reempaqueta entonces el AIP con nueva_version_indice: storage exige que el índice sea distinto
del embebido y el más reciente protegido, y el reempaquetado deja en el AIP y en preservacion_evento un evento
PREMIS de modificación de metadatos que enlaza el resolucion_id.
Consecuencias¶
- Migración archive 084 (tabla, trigger del amarre reemplazado —que exige además que el snapshot nuevo sea
exactamente el del acto—, cola WORM, motivo
nueva_version_indice) y storage 026 (aip.motivo, columna ensanchada aVARCHAR(32)) y 027 (índice versionado). Sin backfill. - Re-amarrar cambia el contenido del índice (lleva
serie_idy nombres): se emite una versión nueva, se sella tras el commit y se protege bajo WORM; la firmada anterior se conserva (append-only, WORM). - Nueva métrica de agotamiento
extension(expediente_worm_extension.extension_agotada).
Deuda declarada¶
- La renovación rodante de un índice CT alcanza las versiones anteriores solo por la cascada desde la raíz; no hay prueba contra un almacén S3 real del circuito completo (la integración de storage usa el SQL real y un almacén en memoria).
- La ejecución del lote comprueba la compuerta antes de purgar el primer expediente, no entre expedientes: una marca que llegue a mitad de una ejecución se verá en la siguiente.
- Un cerrado bajo
migrada(cerrado_bajo_serie_migrada) relevado después con acto retroactivo conserva el motivo viejo (AND NOT trd_revision_requerida); necesita su propia salida. - TVD: solo (a). Re-amarrar una TVD exigiría decidir el hold de valoración y la
fecha_extrema_final. - El acta del Comité es texto: no se adjunta el documento ni se exige radicado del acta.
- La extensión WORM agotada se ve en los logs, el asiento
archive.worm_extension_agotaday la métrica; no hay pantalla. - ~~Zona por despliegue (
TZ_RADICACION), no por tenant~~ — saldada por ADR-062: la zona es de la entidad. expedientes.dispositionsigue sin trigger propio: solo el re-amarre lo ata al amarre.
Enmienda del lote 4 (2026-10-30)¶
- TRD-09:
_eliminar_expedientere-compruebatrd_revision_pendiente/indice_sin_wormpor expediente bajoFOR KEY SHAREsostenido hasta la lápida; el marcado se omite con sumotivoen el informe y su asiento, sin abortar el lote. - TRD-06: listado
GET /expedientes/indices/extensiones-worm-agotadasy reintento manualPOST /expedientes/{id}/worm-extension/{artefacto}/reintentar(asientoarchive.worm_extension_reintento_manual). - TRD-11: migración tenant
086,trg_expedientes_disposition_inmutable: solo el re-amarre de su misma transacción reescribedispositionde un cerrado. - TRD-08: todo «hoy» de negocio de archive sale de
app/core/hoy.py(zona de la entidad). SQLCURRENT_DATEqueda como deuda.
Enmienda de la destrucción en fases (2026-10-30)¶
La enmienda anterior sostenía el FOR KEY SHARE «hasta la lápida y el borrado», es decir, durante la llamada HTTP irreversible de purga a document-service: una transacción abierta a merced de un tercero. Se rediseña en tres fases idempotentes (eliminacion_lote_progreso, migración 088):
- Intención (transacción corta,
FOR KEY SHARE): re-chequeo de la marca (trd_revision_pendiente/indice_sin_worm) + registro de la intención con los radicados a destruir; COMMIT. - Purga sin transacción; se guarda el recuento real.
- Cierre (transacción corta): lápida + soportes + borrado + asiento.
Decisión: el re-chequeo de la fase 1 es el punto de no retorno. Un expediente que lo supera estaba legítimamente a disposición; una marca que llegue después no puede dejar una destrucción a medias, así que (a) el reintento retoma la purga sin volver a consultar la marca, (b) marcar_revision_por_fin_de_vigencia no marca un expediente con intención registrada y (c) anular se rechaza con una intención registrada. Marca e intención son mutuamente excluyentes en ambos órdenes de llegada: si la marca gana el bloqueo, la ejecución la ve y omite el expediente; si gana la intención, la marca no se escribe. Para que (b) sea cierta bajo READ COMMITTED, marcar_* bloquea (SELECT … FOR UPDATE) en una sentencia y escribe en otra: con una sola, el UPDATE que esperó el bloqueo re-evalúa solo la fila y no el subselect NOT EXISTS, y marcaría un expediente cuya destrucción ya empezó (probado con dos conexiones reales).
Omitidos (art. 4.5.4 num. 6-7 y par. 1). El expediente omitido no es un fallo: el lote termina ejecutado_parcial, el acto de retirarlo del lote (motivo obligatorio, crud≥4) lo libera para resolver su revisión, y un addendum al acta —sellado y bajo WORM como ella— consigna lo realmente eliminado y lo omitido.