ADR-063: Pertenencia viva a un lote de eliminación, recuento de la lápida desde la intención y descarga del addendum¶
Estado: Aceptado
Derogado en parte por ADR-065: inventario y recuento desde la intención, `eliminacion_lote_progreso` y la lápida. Siguen vigentes pertenencia viva, retiro y re-entrada, y descarga del addendum.
Fecha: 2026-11-03
Autores: Giampiero (mantenedor principal); salda tres huecos que la 088 dejó declarados en los lotes de eliminación
Relacionados: ADR-061 (ejecución en fases, ejecución parcial, addendum), ADR-059 (instrumento convalidado), ADR-044 (search_path), ADR-053 (criterio del MCP)
Contexto¶
Tras el ADR-061 (migración 088) el lote de eliminación puede quedar ejecutado_parcial, el Comité retira del lote
los expedientes omitidos y el acta gana un addendum. Quedaron tres huecos medidos en el código:
- Un expediente retirado no puede volver a proponerse. La admisión (
lote_existente) miraba cualquier ítem de un lote con estado distinto deanulado, sin descontar el retiro; cuando el lote cerraba (ejecutado, terminal) la condición era permanente (ya_en_otro_lote). Además nada impedía en el motor que un expediente estuviera en dos lotes (la regla vivía en unSELECTde admisión: una carrera entre dos propuestas colaba ambas) y el predicado «en un lote vivo» estaba escrito cinco veces y no coincidía (el trigger de préstamo contaba el parcial, el pre-chequeo físico y la transferencia no; la resolución de revisión contaba<> 'anulado'). - La lápida podía afirmar 0 para siempre.
anexos_eliminadosse sumaba de lo que respondía el último intento de purga; si el intento 1 destruía los bytes y moría antes de responder, el 2 encontraba todo borrado y document-service respondía 0/0. La ramaintentos_purga > 1arreglaba solo los radicados. - El acta y el addendum no decían la serie ni el acto, y no había descarga. Solo
serie(código); el XML firmado y el addendum solo se leían dentro de un JSON.
Decisiones¶
D1. Una tabla, una fila por expediente: eliminacion_expediente_en_lote (migración archive 089).
expediente_id es la PK (ON DELETE CASCADE desde expedientes), con lote_id y item_id (único,
ON DELETE CASCADE). Todo su ciclo de vida lo lleva el motor:
| Evento | Efecto | Mecanismo |
|---|---|---|
| Proponer | alta de la pertenencia junto al ítem | trigger AFTER INSERT en eliminacion_lote_items; el conflicto de PK (23505, eliminacion_expediente_en_lote_pkey) es la carrera: la propuesta perdedora revierte entera y responde 409 expediente_en_otro_lote |
Quitar un ítem de un borrador |
baja | ON DELETE CASCADE desde el ítem |
| Anular el lote | baja de todo el lote | trigger AFTER UPDATE OF estado (anulado) |
| Retirar un omitido | baja de ese expediente | trigger AFTER UPDATE OF estado en eliminacion_lote_omision (retirado) |
| Destruir el expediente | baja | ON DELETE CASCADE desde expedientes |
Lote → ejecutado |
debe quedar vacío | trigger BEFORE UPDATE OF estado (eliminacion_lote_ejecutado_con_pertenencia) |
Un DELETE o UPDATE directo se rechaza (eliminacion_pertenencia_directa; solo se admite pg_trigger_depth() > 1,
mismo molde que la 075). La tabla no es evidencia (lo son el inventario, la omisión y su retiro). Los cinco
consumidores leen esta fuente: lote_existente de la admisión, prestamo_guard_insert, FisicoRepository._EN_LOTE, la
transferencia (asegurar_soporte_citable) y ResolucionRevisionRepository.impedimentos. Efecto colateral deseado: un
omitido sin retirar de un parcial cuenta como «en lote» en todos. El guardián
test_un_solo_predicado_de_lote_vivo.py falla si un consumidor vuelve a recorrer los ítems por estado.
Alternativas descartadas: corregir el SELECT y bloquear expedientes FOR UPDATE al proponer (sigue repetido cinco
veces y solo protege si todo escritor bloquea); un trigger con advisory lock por expediente en los ítems (no unifica el
predicado).
D2. La re-entrada se admite sola. Retirado el omitido, la pertenencia desaparece y la admisión vuelve a evaluar todo.
Para que un re-amarrado cuyo índice aún no está bajo WORM se rechace con nombre y por ítem (y no tumbe la propuesta
entera), list_candidatos_eliminacion trae reamarrado e indice_con_worm y _motivo_de_rechazo gana indice_sin_worm
justo después de trd_revision_pendiente; exigir_lote_sin_revision queda como red dentro de la transacción. El ítem
nuevo congela retirado_de_lote_codigo y retirado_causa (el último retiro) y el acta los imprime en <antecedente>
solo si existen.
D3. El recuento de la lápida sale de la intención. eliminacion_lote_progreso.anexo_ids UUID[] se fija con
radicado_ids al registrar la intención y el trigger de la 088 los hace inmutables. La fase 1 pide el inventario de
anexos a document-service fuera de la transacción (anexo_id aditivo en /internal/documentos/anexos: dos radicados
que comparten bytes son dos anexos), y la transacción corta re-lee los radicados: si difieren de los inventariados, el
intento falla y el reintento recomienza. La fase 2 envía anexo_ids (campo obligatorio de PurgaInternalRequest, no un
interruptor) y document-service, antes de tocar bytes, responde 409 inventario_anexos_cambio si hay un anexo vivo
fuera de ese inventario (un reintento tras una purga a medias tiene menos, nunca más). La fase 3 escribe
cardinality(radicado_ids) y cardinality(anexo_ids), siempre; lo que respondió el último intento queda como
diagnóstico (confirmado_ultimo_intento en el asiento archive.expediente_eliminado_trd).
Semántica que cambia: «anexos eliminados» cuenta anexos (registros) del expediente, no objetos del almacén; los bytes
compartidos con un radicado vivo sobreviven pero el anexo del expediente sí se destruye.
Descartado: un libro de purgas por expediente en document-service (reconstruye en otro servicio lo que la intención ya
puede fijar, y un intento que muera antes de escribir su libro reabre el mismo hueco).
D4. Serie, subserie, acto y acta de aprobación congelados al proponer. El ítem guarda trd_serie_nombre,
trd_subserie_nombre (solo si el expediente se amarró a una subserie), trd_acto_convalidacion y trd_acta_aprobacion
(el acta y la fecha de aprobación del Comité de esa versión de TRD/TVD que exige el art. 4.5.4 par. 1), tomados de la instantánea del expediente (la
versión de TRD que lo gobernó, no la vigente al ejecutar). Un CHECK … NOT VALID exige serie, acto y acta de aprobación en toda fila nueva (si faltan, el ítem se rechaza por nombre con instrumento_sin_datos; el CHECK es la red y responde 409) sin
tocar las existentes. El acta y el addendum imprimen <serieNombre>, [<subserieNombre>] y <actoConvalidacionTrd>
tras <serie> (más <actaAprobacionTrd>); el addendum los une al inventario por expediente_id y omite los elementos de un ítem anterior a la 089 en vez de firmarlos vacíos. El <antecedente> cubre también el último lote anulado tras publicarse (causa = lote_anulado). El espacio de nombres
(urn:orpycamcp:acta-eliminacion:v1) no cambia: los elementos son aditivos y las actas ya firmadas no se regeneran.
D5. Descarga mediada del acta y del addendum. GET /eliminacion/lotes/{id}/acta/descarga, …/acta/addenda/{n}
(JSON) y …/acta/addenda/{n}/descarga, con ?version=firmada|original. archive media los bytes (storage sigue
sellado): original es el XML que ancla el SHA-256; firmada es el artefacto XAdES del almacén (el vigente si la firma
tiene renovación LTA; solo si signature-service falla al resolverlo (no si simplemente no hay renovación), el firmado original con X-Orpyca-Advertencia: artefacto_vigente_no_resuelto,
que el gateway ahora reenvía). Se sirve application/xml, Content-Disposition: attachment, Cache-Control: no-store y
ETag = SHA-256 del cuerpo. El umbral de lectura (el nivel más alto del inventario) va en el WHERE de la
consulta, no en una comprobación previa; sin fila, una segunda lectura sin umbral distingue 404 de 403
(acta_sobre_su_clearance, que se conserva porque quien pregunta ya ve el lote). get_acta_eliminacion se reescribe con
el mismo patrón. Cada descarga exitosa deja archive.eliminacion_acta_descargada (best-effort, con actor y canal).
D6. MCP. Se ofrece consultar_addendum_acta_eliminacion; la descarga del XML firmado no se ofrece (código BIN: los
bytes firmados se verifican sobre su contenido exacto y una conversación no los transporta).
Consecuencias¶
- Migración archive 089, idempotente y sin backfill: aborta con un mensaje claro si el esquema tiene un lote vivo
con expedientes aún existentes (no puebla una tabla de invariante con datos previos; anule o ejecute esos lotes, o
recree el entorno de pruebas). El tenant
demono tiene lotes de eliminación. - document-service:
AnexoInternalItem.anexo_id(aditivo) yPurgaInternalRequest.anexo_ids(obligatorio): archive y document se despliegan juntos. - Errores nuevos:
expediente_en_otro_lote,inventario_anexos_cambio,addendum_no_encontrado,acta_sin_firma,artefacto_no_disponible; motivo de rechazoindice_sin_worm.
Deuda declarada¶
- Si aparece un anexo nuevo en un radicado ya inventariado,
inventario_anexos_cambiose repite en cada reintento (la intención es inmutable): hoy exige intervención del operador. En un expediente cerrado no ocurre. - No se informa cuántos objetos del almacén se conservaron por estar compartidos (dato que hoy solo sale en un log de document-service).
- Las actas ya firmadas sin
serieNombre/actoConvalidacionTrdno se regeneran (append-only y WORM): solo los lotes propuestos tras la 089 los traen. - El addendum sigue sin firma personal (solo el sello institucional), igual que el acta.