Skip to content

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:

  1. Un expediente retirado no puede volver a proponerse. La admisión (lote_existente) miraba cualquier ítem de un lote con estado distinto de anulado, 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 un SELECT de 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').
  2. La lápida podía afirmar 0 para siempre. anexos_eliminados se 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 rama intentos_purga > 1 arreglaba solo los radicados.
  3. 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 demo no tiene lotes de eliminación.
  • document-service: AnexoInternalItem.anexo_id (aditivo) y PurgaInternalRequest.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 rechazo indice_sin_worm.

Deuda declarada

  • Si aparece un anexo nuevo en un radicado ya inventariado, inventario_anexos_cambio se 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/actoConvalidacionTrd no 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.