ADR-032: La dependencia se correlaciona por código, no por nombre¶
Estado: Aceptado Fecha: 2026-10-10 Autores: Giampiero (mantenedor principal); triage B5 Relacionados: ADR-012 (migraciones), ADR-028 (el flujo mueve el estado), ADR-029 (ubicación actual), ADR-010 (sobre de eventos)
Contexto¶
Orfeo identifica una dependencia por DEPE_CODI (numérico): radi_depe_actu, USUARIO.DEPE_CODI y el
historial usan el código; el nombre (DEPE_NOMB) solo se une para mostrar, y renombrar no comprueba
nada porque nada se une por nombre. OrpycaMCP heredó el código en casi todo (urd_usuaroldep.depe_id,
radicados.*_dept_code, flujo_dept_code, archive, buzones), pero el flujo no: flow_steps.to_dept,
workflow_rules.assign_to_dept y la bandeja (to_dept IN (SELECT d.nombre …)) se unían a dependencias
por nombre. Consecuencias medidas:
- Renombrar una dependencia con trámite vivo dejaba sus pasos fuera de toda bandeja y de la
ubicación; por eso tenant-service lo prohibía con un
409(divergencia del legado, que renombra libre). - Dos dependencias homónimas se confundían:
borrador_serviceelegíaORDER BY codigo LIMIT 1y radicaba hacia la de menor código en silencio;notificationtomaba una fila arbitraria. dependencias.codigoera editable porPATCHy 8+ tablas de 5 servicios lo citan sin FK: cambiar el código rompía todo en silencio.nombreadmitía 500 caracteres y las instantáneas sonVARCHAR(100).
Decisión¶
1. La llave es dependencias.codigo (INT); el nombre es una instantánea¶
Alternativa B (UUID dependencias.id) descartada: introduce una tercera identidad cuando el sistema ya
habla código, obliga a un JOIN UUID↔código en cada consulta de bandeja (urd_usuaroldep.depe_id es el
código) y deja sin proteger las 8 tablas que ya lo citan.
*_dept_codees el campo canónico;*_dept(nombre) queda como instantánea del momento del acto. Las respuestas llevan siempre ambos.codigoes inmutable:PATCHcon otro valor ⇒422 dependencia_codigo_inmutable(el mismo valor es un no-op: el formulario lo reenvía) y, como defensa en profundidad, un triggerBEFORE UPDATE OF codigo(ERRCODE check_violation) cubre scripts y futuros endpoints. Corregir un código mal tecleado = crear la dependencia nueva y desactivar la vieja. Se descartó «409 solo si hay referencias»: las referencias viven en 8+ tablas sin FK y una lista de tablas se queda corta en silencio.nombre≤ 100 (CHECK+ Pydantic): la migración aborta listando las que exceden.
2. Una sola definición del criterio nombre → código¶
resolver_codigo_dependencia(nombre, historico) (migración tenant 013, en el schema de cada tenant)
devuelve (codigo, candidatos); el código sale solo con exactamente un candidato. historico=false
(alias de API): dependencias activas con ese nombre actual. historico=true (backfill y replay):
activas, inactivas, nombres anteriores y mapeos manuales. dependencia_nombre_historico es de solo
inserción: el renombre guarda el nombre anterior en la misma transacción que el asiento; el mapeo manual
(POST /dependencias/{id}/mapeos-nombre, USUA_PERM_ADMIN nivel 3, asentado) es la decisión humana para
un nombre ambiguo o huérfano. Nunca se desempata.
3. Contrato: código canónico, nombre alias deprecado¶
Un ciclo de convivencia, resuelto con historico=false: 1 candidato ⇒ su código; 0 ⇒ 422
dependencia_desconocida; ≥2 ⇒ 422 dependencia_ambigua (con candidatos); código y nombre que no
concuerdan ⇒ 422 dependencia_inconsistente. El nombre que se guarda lo pone el servidor desde el
catálogo, nunca el texto del cliente. El uso del alias responde Deprecation: true y
Link: </docs/es/api.md#dependencias-por-codigo>; rel="deprecation" y deja un warning en el log (mide
cuándo retirarlo). El origen de un paso ya no lo aporta el cliente: lo deriva el servidor del paso
activo; si el cliente lo envía y no coincide ⇒ 409 origen_desactualizado. Las bandejas aceptan
?dept_code= (?dept= es el alias; ambiguo es 422, ya no «lista vacía»). Las condiciones de reglas
eq/ne/in sobre dest_dept/origin_dept se rechazan (422 condicion_por_nombre_obsoleta: cambiaban de
sentido al renombrar) y se ofrece dest_dept_code/origin_dept_code; contains/regex sobre texto siguen.
Eventos: workflow.* y document.radicado.created llevan ambos campos (ADR-010: aditivo); los
consumidores prefieren el código y caen al nombre resuelto con historial (así un evento viejo de la
cola de fallidos con el nombre anterior a un renombre sigue llegando a su dependencia).
4. Migraciones con backfill que falla cerrado¶
tenant 013 (trigger, historial, función), document 046 (borrador.dest_dept_code, relleno de
radicados.*_dept_code), workflow 019 (columnas *_code en pasos, eventos y reglas; relleno;
condiciones; ubicación; restricciones). Reglas: se escribe solo con un candidato; un paso vivo, una
regla o una condición eq/ne/in con nombre ambiguo/huérfano aborta la transacción (la migración no queda
en el libro; el remedio es el mapeo manual y reejecutar); en historia (pasos terminales, from_dept,
flow_events) el código queda NULL con evidencia, porque la historia no se adivina. flow_events es
append-only: la 019 aparta el trigger dentro de la transacción, escribe solo las dos columnas nuevas, lo
rehabilita y verifica que quedó habilitado. Cada migración deja evidencia de solo inserción y el runner
la ancla en audit_log (conteo + SHA-256 del contenido ordenado). Después de la 019 un paso vivo sin
código no puede existir (CHECK), ni una regla sin código destino.
5. Desactivar y eliminar¶
Renombrar ya no se bloquea. Desactivar conserva el 409 dependencia_con_tramite_en_curso, ahora por
código (la bandeja filtra d.activo: el trámite quedaría invisible; divergencia deliberada del legado,
que desactiva sin comprobar). Eliminar da 409 dependencia_con_historia si cualquier tabla la cita
(pasos, eventos, reglas, radicados, borradores, URD, expedientes): una dependencia usada no se borra
(trazabilidad, Ley 594). La lista de tablas no es a mano: referencias() barre
information_schema del schema del tenant buscando columnas enteras *dept_code, *depe_id,
*depe_codigo, *dependencia*_codigo, oficina_productora_codigo y unidad_administrativa_codigo
(más dependencia_nombre_historico.codigo), así que una tabla nueva de otro servicio queda cubierta y
una dependencia con renombres o mapeos tampoco se elimina. Corrección (ronda de auditoría): la versión
inicial de este párrafo afirmaba que, al añadir tablas, «el fallo es no deja borrar»; era al revés
con una lista a mano —lo que faltaba se contaba como cero y el borrado pasaba—. Hoy el fallo de una
omisión del barrido sería «borra con referencias», por eso el barrido es por catálogo y la prueba de
integración fija cada columna.
Un código eliminado no se reutiliza. DELETE escribe, en la misma transacción que el asiento
dependencia.eliminada, el código en dependencia_codigo_retirado (solo inserción); crear una dependencia
con un código retirado, o con historial de nombres y sin dependencia viva, da 409
dependencia_codigo_retirado (y un trigger BEFORE INSERT lo impide aunque se salte la API). Sin esto,
una dependencia nueva heredaría los trámites viejos que citan ese código.
Mapeo manual (POST /dependencias/{id}/mapeos-nombre). Con p_historico el mapeo es la
respuesta de resolver_codigo_dependencia para ese nombre (gana a las coincidencias textuales; dos
mapeos con códigos distintos siguen siendo ambiguos); antes se sumaba al UNION y no podía desambiguar
homónimas. Es irreversible (el historial es de solo inserción y no hay endpoint para deshacerlo), por
eso 409 dependencia_inactiva si el destino está inactiva y 409 nombre_de_otra_dependencia si el
nombre es el vigente de OTRA dependencia activa (salvo que el destino también lo lleve: homónimas, el
caso que el mapeo existe para resolver). El PATCH que renombra admite un motivo opcional (acto de
reestructuración), guardado en el historial y en el asiento.
Solo nombres huérfanos o ambiguos. El mapeo es la salida del RAISE de los backfills, no una vía para
redirigir un nombre que ya resuelve: dentro de la transacción, antes de insertar, se consulta
resolver_codigo_dependencia(nombre, true) y se rechaza con 409 nombre_resuelto_a_otra_dependencia si
da exactamente un candidato y no es el código de la dependencia destino (huérfano = 0 candidatos y
ambiguo = más de 1 siguen admitidos). El PATCH que renombra rechaza con 409
nombre_mapeado_a_otra_dependencia un nombre nuevo que ya tiene un mapeo manual hacia otro código.
Evidencia de los backfills (ronda de corrección). La evidencia anclada en audit_log (046 document,
019 workflow) no se reescribe: las migraciones 047 (document) y 020 (workflow) solo completan columnas
NUEVAS, fuera del hash de las anclas originales, y cada una deja su propia ancla
(document.dependencia_codigo.backfill_047, workflow.dependencia_codigo.backfill_020). «Mapeado» no es un
valor de resultado en document: se deriva de historico_id IS NOT NULL (en workflow la 019 ya lo escribió
y la 020 solo enlaza historico_id). El despues que la 020 reconstruye es la regla vigente al
migrar (reconstruido = TRUE, no una instantánea del momento del backfill); lo que no se puede probar
queda NULL. Un mapeo registrado después de la 046/019 no se refleja en la evidencia (deuda: job de
reparación).
6. Correcciones colaterales¶
borrador_service ya no elige ORDER BY codigo LIMIT 1 (un nombre ambiguo es 422); notification
resuelve el buzón por código y, sin él, por el criterio único con historial (ya no una fila arbitraria);
el webhook recorta también to_dept_code/from_dept_code/origin_dept_code/assign_to_dept_code
(admisión inversa); el reporte por dependencia agrupa por código.
Consecuencias¶
Positivas: renombrar una dependencia con trámite vivo ya no rompe bandeja, tenencia ni ubicación; las homónimas dejan de confundirse; el código deja de poder cambiar bajo 8 tablas sin FK.
Negativas / límites:
- Un tenant con pasos vivos de nombre ambiguo no migra hasta que un administrador registre el mapeo.
- Las condiciones textuales (contains/regex) sobre un nombre siguen cambiando de sentido al renombrar:
se listan en la evidencia (conditions:textual) y se documentan; no se bloquea el renombre por ellas.
- Un paso histórico sin código no se puede reactivar por rollback (el CHECK lo impide: 409
paso_anterior_sin_codigo).
- El alias por nombre debe retirarse en un incremento posterior (cuando el Deprecation esté a cero
durante un ciclo): quitar el alias de los schemas y los índices por nombre (ix_flow_steps_to_dept,
ix_flow_steps_inbox); la función con historico=true se queda para el replay.
- La copia de resolver_codigo_dependencia que usan los tests de integración de document y workflow debe
coincidir con la de tenant-service: test_dependencia_codigo.py fija los mismos casos contra la real.