ADR-058: Gate de soporte y formato para Salidas e Internas¶
Estado: Aceptado Fecha: 2026-10-29 Autores: Giampiero (mantenedor principal); épica «gate de formato», salda la deuda declarada en ADR-057 §5 Relacionados: ADR-057 (soporte y formato por tipo), ADR-025 (TRD versionada), ADR-053 (MCP), ADR-023 (preservación)
Contexto¶
ADR-057 hizo que cada tipo documental declare el soporte y los formatos en que se produce, pero dejó la radicación sin
bloqueo: solo un derivado de lectura (conformidad_tipo). Esa decisión era correcta para las Entradas, que vienen de
fuera y que la ventanilla no puede devolver por su extensión. No lo es para lo que la entidad produce (Salidas e
Internas): el Ac. AGN 001/2024 art. 4.1.4 obliga a planear la producción en la TRD (y su parágrafo obliga a incorporar, desde la planeación y la producción, mecanismos que garanticen la fiabilidad, autenticidad, integridad, veracidad e inalterabilidad; verificar lo producido contra lo planeado es uno de esos mecanismos), y una TRD que dice «Oficio en PDF/A»
mientras el sistema radica un .docx sin que nadie lo note es una planeación decorativa.
El evaluador heredado tenía cinco defectos que habrían convertido el gate en una fuente de falsos veredictos:
pdfa_status = 'pendiente'(la validación asíncrona aún no termina) caía en «fuera de formato».- Un código de formato fuera del catálogo se descartaba en silencio, de modo que un tipo «solo X» se leía como «no declarado» y pasaba.
- Un anexo sin MIME se saltaba (
continue) y se colaba. - El MIME evaluado era el que copiaba
anexos.mime_typedeAnexoInput.mime_type, enviado por el cliente. - El derivado fechaba con
registered_at.date()(UTC) y la radicación decide el día en la zona de radicación: entre las 19:00 y las 24:00 de Bogotá el derivado y el gate habrían mirado TRD de días distintos.
Decisión¶
1. Qué actos se gatean¶
| Acto | ¿Gate? |
|---|---|
Radicar Salida/Interna (POST /documents, respuesta rápida, borrador E23) |
Sí, antes de la transacción del número |
Lote Salida/Interna (POST /batch/documents) |
Sí, por ítem (sin anexos juzga solo el soporte) |
Retipificar (PATCH /documents/{id} con doc_class distinto) |
Sí, con los anexos ya guardados y la TRD a la fecha del radicado |
corregir_conservacion |
No bloquea, asienta el veredicto (corrige un hecho; bloquearlo obligaría a mantener un medio_conservacion falso) |
| Firmar / enviar | No: el contenido ya es inmutable (no existe un acto que añada o cambie anexos tras radicar; un guardián lo mantiene) |
| Importación INT-05, Entradas, correo, ventanilla | No: traslado de un registro histórico, o viene de fuera |
El gate corre después de los gates existentes (tipo, metadatos, alcance de anexos) y antes de next_sequence: un
rechazo no cuesta número, que no se devuelve (numeración única e inmutable: Ac. AGN 001/2024 art. 4.2.3). En E23 hay además una pre-verificación antes del render
(se espera pdfa) y la definitiva en create con el formato que declaró el renderizador (borrador.documento_formato).
2. Un solo evaluador¶
app/core/conformidad_tipo.py (puro): evaluar_conformidad devuelve un Veredicto rico; el derivado de lectura y el gate
pasan por ConformidadService (único módulo que lo importa; guardián G1). El MIME lo resuelve el servidor desde
files.mime_type (storage): AnexoInput.mime_type se retira del contrato (Pydantic ignora el campo si un cliente viejo
lo envía; sin rama de compatibilidad).
Storage olfatea el contenido al subir (app/core/contenido.py): %PDF-, PNG, JPEG, GIF, TIFF, ZIP (y dentro de él OOXML por
[Content_Types].xml + word//xl//ppt/, ODF por la entrada mimetype) y OLE (Office 97-2003). Si el contenido es de otro
formato que la lista admite, se guarda el detectado (no se rechaza: un adjunto de correo mal rotulado no se pierde y lo que queda
registrado es lo que el fichero ES); si el declarado se puede comprobar y el contenido no lo cumple ni es otro formato admitido,
se rechaza (415). Deuda declarada: text/plain, text/csv, message/rfc822 y application/xml no tienen firma que
comprobar y se conservan como los declaró el cliente; con la lista abierta (ALLOWED_MIME_TYPES=*) storage no olfatea nada y registra el MIME declarado (la entidad que la abre renuncia a esta garantía); los OLE (doc/xls/ppt) se distinguen de lo demás pero no entre sí.
Lo que declara el renderizador se normaliza en un solo sitio (formato_producido_normalizado: minúsculas y solo alfanuméricos):
storage emite PDF/A-2B (perfil.upper()) y la comparación inicial con startswith("pdfa") no casaba, de modo que todo E23 de
una Salida/Interna se rechazaba tras renderizar. Las pruebas no lo vieron porque los unitarios usaban un valor inventado
(pdfa-2b) y la integración de E23 solo persistía documento_formato sin pasar por el gate; ahora una prueba de integración corre
create y el anexado con el literal real.
3. Casos límite¶
- Más de un candidato (el
codevive en varias series y el radicado guarda el código, no el tipo): se evalúa cada uno y bloquea solo si ninguno admite (veredicto firme para cualquier serie); si alguno admite, pasa conambiguo_admitido. - No declarado (tipo suelto,
migradasin captura, instantánea previa a la 082): ese candidato admite (no_declarado); la omisión la cierra el409 tipo_documental_sin_soporteal aprobar. Sin TRD vigente a la fecha: pasa (sin_trd_vigente). - Original en papel (
medio_conservacion = fisico): se juzga solo el soporte; los anexos son reproducciones. - PDF/A (el tipo admite
pdfay nopdf):conformantconforma;non_conformant→ 422;pendiente→ 409 conRetry-After: 5; sin veredicto (NULL/not_evaluated) → 409 fail-closed. Con el validador apagado (por defecto) un tipo «solo PDF/A» no admite PDFs subidos a mano: el remedio es activarPRESERVATION_PDFA_VALIDATION_ENABLEDcon veraPDF o que la TRD declare tambiénpdf. Si el tipo admitepdf, cualquierapplication/pdfconforma. - PDF/A producido por el sistema (E23): excepción explícita y acotada a ADR-057: un anexo
producidocuyo formato empieza porpdfacuenta comopdfa(y, siendo PDF, comopdf). La procedencia la identifica el servidor (borrador.documento_file_id), nuncatipo_anexo(lo manda el cliente). - Declaración ilegible (código fuera del catálogo,
filesinaccesible, error al leer la TRD): 503 fail-closed; pasar sería afirmar conformidad sin haberla leído. Un anexo sin MIME: 409mime_desconocido. Unfile_idausente defiles: el mismo 422 neutroanexo_no_disponible(sin oráculo). - El plan de preservación no se consulta: alimenta los avisos del Comité (ADR-057 §2.5) y cambia con el tiempo; el gate juzga contra la TRD convalidada.
- Una sola versión por serie, y el catálogo no anula la TRD (ronda de auditoría 2; el desempate por versión fue reemplazado por ADR-059): si dos versiones de la misma serie cubren
la fecha (acto retroactivo de la N+1 y N derogada con
valid_to = hoy) rige la de vigencia mayor (fecha_convalidacion, luegoversion); un tipo suelto (sin serie) solo cuenta cuando no hay ningún candidato de TRD para elcode, de modo que crear un suelto homónimo ya no esquiva el plan convalidado; si el único candidato es un suelto, el 422 dice «el catálogo de tipos», no «la TRD vigente». Al convalidar la N+1 sobre unamigrada, archive congela la instantánea de lamigradaantes de re-apuntar sus tipos, y el gate la sigue leyendo (antes quedaba ensin_trd_vigentey retipificar pasaba sin juicio). - Soporte del original en la respuesta rápida y en el lote: ambos aceptan
medio_conservacion(defectoelectronico); sin él un tipo planeado solo en papel no tenía salida desde la bandeja ni por lote. - Fecha: un solo helper,
fecha_de_radicacion, en la zona de radicación (defecto 5).
4. Contrato¶
422 formato_no_planeado_en_trd— definitivo: el cliente cambia el payload (anexo, soporte o tipo).409 formato_no_verificable—motivo ∈ {pdfa_pendiente, pdfa_sin_veredicto, mime_desconocido};Retry-After: 5solo conpdfa_pendiente(reintento idéntico).503 conformidad_no_verificable—motivo ∈ {codigo_fuera_de_catalogo, files_inaccesible, lectura_trd, anexo_ausente_de_files}.
Los tres traen conformidad {motivo, doc_class, fecha_trd, candidatos, medio_conservacion, soportes_admitidos,
formatos_admitidos, anexos_rechazados[{file_id, mime_type, motivo}]}. El rechazo no deja asiento (no hubo acto ni
mutación): lo probatorio es lo que pasa. document.radicado_created, document.radicado_actualizado (retipificar), el asiento
del ítem del lote y document.conservacion_corregida llevan conformidad_formato {veredicto, fecha_trd, candidatos,
formatos_admitidos, soportes_admitidos, candidatos_detalle[{trd_serie_id, version, origen (convalidada|migrada|suelto),
resultado}], anexos[{file_id, mime, formato (código del catálogo), pdfa_status, pdfa_profile, origen}]} con
veredicto ∈ {conforme, solo_soporte, ambiguo_admitido, no_declarado, sin_trd_vigente} (y en la corrección también
no_conforme/indeterminado/ilegible). solo_soporte = pasó el soporte pero no se juzgó ningún formato (original en papel,
o electrónica sin anexos, lote incluido): asentar conforme afirmaría un formato que nadie vio. En E23 el asiento de creación
juzga el formato del primer render y el anexado deja document.documento_producido_adjuntado con el file_id real y el
veredicto del formato FINAL (con diferidos el definitivo es el segundo render); no bloquea, el número ya se consumió.
Sin oráculo: el 422 solo muestra el mime_type de un fichero que subió quien radica, que ya es anexo del radicado o cuando
no hay actor humano; el de un fichero ajeno sin radicado va en null. La lectura del gate corre en un savepoint: un error de
Postgres no deja abortada la transacción del acto (corregir_conservacion daba 500). El lote falla el ítem con
error = <código>: <detalle>. Retry-After ya cruza el gateway; no hay endpoint nuevo ni «pre-validar» (el 422 lleva todo).
5. Frontend y MCP¶
/radicar,/borradores,/bandeja(respuesta) pintan un aviso persistente (RechazoFormato.svelte) con lo que la TRD planea y los anexos señalados (porfile_id); conpdfa_pendienteofrece «Reintentar» trasRetry-After. Los proxies BFF reenvíanRetry-After. La UI no decide: muestra, de forma informativa, los formatos que declara el tipo elegido.- MCP: sin tools nuevas (censo en 0 huecos). Las descripciones de
radicar_documento,tipificar_radicado,crear_respuesta_vinculada,radicar_borradory del procedimientodespachar_respuestaexplican los tres códigos y qué hacer (no se reintenta a ciegas).
Consecuencias¶
- Sin migración y sin backfill: lo ya radicado no se reevalúa; solo las radicaciones nuevas, los lotes y las retipificaciones.
- Con la configuración por defecto (validador PDF/A apagado), una TRD que declare solo
pdfarechazará con 409 los PDFs subidos por una persona. Es deliberado: el gate existe para que un PDF no pase por PDF/A sin que nadie lo haya verificado. Las semillas de la demo y del corpus declaranpdfaypdf. AnexoInput.mime_typedesaparece;conformidad_tipocambia dos semánticas: original en papel →anexos_fuera_de_formato = nullypendiente→null(no «fuera»).- Guardianes: G1 (un solo módulo importa el evaluador, nadie más traduce MIME a formato), G2 (todo punto que pide consecutivo
llama antes a
exigir), G3 (los anexos solo se insertan en tres sitios).
Deuda declarada¶
Lote 6 (2026-10-30) salda del olfateo: un OLE declarado sin firma OLE se rechaza (solo un
.xlsde texto pasa atext/csvsi la lista lo admite) y un.docx/.xlsx/.odtrotuladoapplication/zipse registra siempre como lo que es (415si la entidad no lo admite); el nombre guardado lo fija el servidor (nombre_seguro). Siguen abiertos elfilename/file_sizedeAnexoInputen document-service y el olfateo detext/*.
- Olfateo (storage,
app/core/contenido.py): el PDF se reconoce después de las firmas estructurales (ZIP/OLE/imágenes) y solo al inicio (BOM/espacios) o con basura binaria delante; un ZIP/HTML que lleva%PDF-en el primer KiB ya no es PDF. Un.docxrotuladoapplication/zipse registra como docx si la lista lo admite. Un OLE declarado (xls/doc/ppt) sin firma OLE no se rechaza (CSV que Windows/Outlook rotulan xls, RTF guardado como .doc; un CSV se registratext/csvsi la lista lo admite): se conserva lo declarado, deuda igual quetext/*. - ~~Cambiar la vigencia de las versiones en archive (
valid_tode la N = acto de la N+1, rechazarfecha_convalidacionfutura)~~: saldado por ADR-059. El motor garantiza una sola versión por código y fecha, y el desempate por lectura descrito en «Una sola versión por serie» se retiró. - Olfateo del MIME de
text/plain,text/csv,message/rfc822yapplication/xml(sin firma que comprobar): el gate hereda su fidelidad defiles.mime_type. - Un Radicado S/I electrónico sin ningún anexo no es cuestión de formato y el gate no lo juzga.
AnexoInput.filename/file_sizesiguen viniendo del cliente.- Que el radicado guarde el
iddel tipo (no solo elcode) eliminaría la ambigüedad: cambio de modelo aparte.