CAP-01 · Contexto de ejecución
Estado: Borrador · Hito: M1 · Slice: S1.01 · Fecha: 2026-09-25
Requisitos de la capacidad que toda petición atraviesa antes de llegar al dominio: quién actúa, en qué organización, en qué sede y con qué reloj. Se escribe justo antes de construirla, según MARCO.md parte IV.
Por qué es la primera. Su coste es el más bajo que va a ser: hoy existen un repositorio, un
endpoint y once clases en compartido. Su omisión es transversal —atraviesa las cuatro capas de
los trece módulos— y silenciosa: un error sin contrato devuelve un JSON feo, pero una consulta sin
alcance devuelve datos de otra organización sin que nada falle.
1. Glosario
Términos ya definidos en el marco conceptual §2.1, que no se redefinen aquí: Sede, Sección de laboratorio, Orden de laboratorio.
| Término | Definición | Origen |
|---|---|---|
| Actor operativo | La cuenta_usuario autenticada que ejecuta la operación y a la que se atribuye en auditoría. Es «referencia a una identidad externa; no almacena contraseñas ni secretos reversibles». |
Modelo: comment on table cuenta_usuario |
| Organización | «Raíz de aislamiento de una instalación o entidad legal que opera el LIS.» | Modelo: comment on table organizacion |
| Contexto de ejecución | Actor, organización, sede y reloj, resueltos en el borde y pasados al caso de uso como datos. | Este documento |
2. Reglas (invariantes)
Las que provienen del marco conceptual citan su RN. Las que solo están en el modelo de datos se marcan como tal, con la restricción que las sostiene: el DDL es fuente de reglas por ADR-0003, y no se inventa nada que no esté en uno de los dos sitios.
| ID | Regla | Origen |
|---|---|---|
| BR-01 | Una cuenta_usuario pertenece a exactamente una organización. |
Modelo: cuenta_usuario.organizacion_id not null |
| BR-02 | Una sede pertenece a exactamente una organización, y su código es único dentro de ella. | Modelo: sede_organizacion_codigo_uk |
| BR-03 | Una entidad hija no puede referenciar a un padre de otra organización. | Modelo: los unique (id, organizacion_id) existen para que las claves ajenas compuestas lo impidan, p. ej. cuenta_profesional_fk |
| BR-04 | Una orden pertenece a un paciente y a una sede de registro. | RN-02 |
| BR-05 | Los eventos se guardan con zona horaria; nacimiento y vencimientos son fecha civil. | RN-20 |
| BR-06 | Una cuenta puede estar activa, bloqueada o inactiva; solo la activa opera. |
Modelo: cuenta_usuario.estado |
| BR-07 | Un rol se asigna a una cuenta con vigencia y sin solape para el mismo rol. | Modelo: cuenta_rol_sin_solape_excl |
| BR-08 | El actor referencia una identidad externa; el LIS no almacena su contraseña ni ningún secreto reversible. | Modelo: comment on table cuenta_usuario |
| BR-09 | La organización es la raíz de aislamiento de una instalación o entidad legal. | Modelo: comment on table organizacion |
Dónde está definido el aislamiento, y dónde no. Ninguna de las 28 reglas consolidadas del marco conceptual menciona la organización, y el lenguaje ubicuo (§2.1) no la define. Pero sí está definida en el modelo de datos, en el comentario de su tabla: «Raíz de aislamiento de una instalación o entidad legal que opera el LIS». Por ADR-0003 el DDL es fuente de reglas, así que el aislamiento está escrito y no había que decidirlo.
Ese «instalación o entidad legal» es deliberado: fija que la organización es la raíz de aislamiento, y deja abierto si una instalación aloja una sola o varias. Lo segundo es una decisión de despliegue y no cambia el código, porque el código debe tratarla como raíz de aislamiento en los dos casos.
Lo que sigue siendo un hueco es que el marco conceptual no la recoja: un concepto con definición escrita y presencia en 26 tablas debería estar en el lenguaje ubicuo de §2.1. No lo arreglo aquí —no me corresponde editar el marco conceptual— pero queda anotado. De ahí DR-01.
3. User Stories
| ID | Como | Quiero | Para | Prioridad |
|---|---|---|---|---|
| US-001 | desarrollador del LIS | que ningún identificador cruce el borde del dominio como UUID crudo |
que sea imposible pasar un SedeId donde se espera un OrganizacionId |
Must |
| US-002 | operador autenticado | que el sistema resuelva quién soy y en qué organización estoy sin que yo lo declare | que no pueda actuar en nombre de otra organización | Must |
| US-003 | responsable del laboratorio | que ninguna consulta devuelva datos de otra organización | que el aislamiento no dependa de que cada consulta recuerde filtrar | Must |
| US-004 | desarrollador del LIS | que el tiempo sea una dependencia inyectable | que un test de vigencia no dependa de la hora en que se ejecuta | Must |
4. Criterios de aceptación
Bordes y errores integrados, no en apéndice. Los códigos HTTP concretos se deciden en la arquitectura de errores (ADR-0008, pendiente); aquí se fija el comportamiento, que no depende de ella.
# AC-001 [US-001] — Identificadores tipados
Escenario: Un identificador no se confunde con otro
Dado un caso de uso que recibe una organización y una sede
Cuando se le pasa un SedeId en el lugar de la organización
Entonces el código no compila
Escenario: El identificador no acepta un valor ausente
Cuando se construye un SedeId con un UUID nulo
Entonces falla al construirse, no al usarse
# AC-002 [US-002, BR-01] — Resolución del actor
Escenario: El actor sale de la credencial, no de la petición
Dada una petición autenticada por una cuenta activa
Cuando se resuelve el contexto de ejecución
Entonces el actor es esa cuenta
Y la organización es la de esa cuenta
Y el dominio recibe ambos como datos, sin consultarlos
Escenario: La petición no puede declarar su propia organización
Dada una petición autenticada por una cuenta de la organización A
Cuando la petición incluye un dato que nombra la organización B
Entonces se ignora, y el contexto sigue siendo el de A
Escenario: Cuenta no activa
Dada una petición autenticada por una cuenta bloqueada o inactiva
Cuando se resuelve el contexto
Entonces la operación se rechaza
Y no se ejecuta ningún caso de uso
# AC-003 [US-003, BR-02, BR-03] — Alcance por organización
Escenario: Una lectura solo ve su organización
Dadas dos organizaciones con sedes activas cada una
Cuando un actor de la organización A lista las sedes
Entonces recibe únicamente las sedes de A
Escenario: Un identificador de otra organización no resuelve
Dado un actor de la organización A
Cuando pide un recurso cuyo identificador pertenece a la organización B
Entonces no lo recibe
Y la respuesta no permite distinguir "no existe" de "no es tuyo"
Escenario: Olvidar el alcance no compila ni pasa
Cuando un repositorio expone una consulta que no recibe la organización
Entonces el test de arquitectura o la firma lo impiden # ver OQ-01
# AC-004 [US-004, BR-05] — Reloj
Escenario: El tiempo se inyecta
Dado un caso de uso que sella un instante
Cuando se ejecuta con un reloj fijado a un instante conocido
Entonces el valor sellado es exactamente ese instante
Y está en UTC
Escenario: La fecha civil no se deriva de la hora del servidor
Dado un reloj fijado a las 23:30 del día D en UTC
Cuando se calcula la fecha civil de la operación
Entonces se usa la zona horaria declarada, no la del proceso # ver DR-03
5. Máquina de estados
No aplica. Esta capacidad no tiene entidad con ciclo de vida propio; los estados que toca
—cuenta_usuario.estado— son un catálogo cerrado, no una máquina con transiciones. El motor de
transiciones es S1.03.
6. NFR
Ninguno propio. Aplica el SLO global de 00-agents.md: p95 < 500 ms en
consultas operativas. La resolución del contexto ocurre en cada petición, así que no puede
añadir una consulta por petición: si resolver el actor exige ir a la base, se mide.
7. Dependencias
-
Aguas arriba: la autenticación provisional del perfil
dev(usuario único en memoria). El modelo real de cuentas y roles llega con M8; hasta entonces el contexto se resuelve contra una cuenta sembrada, no contra un proveedor de identidad. Concretamente:datos_demostracion.sqlsiembra la organizaciónLAB-DEMO(zonaAmerica/Bogota) y las cuentasadmin.demoybio.demo, así que el actor se resuelve buscando lacuenta_usuariocuyonombre_accesocoincide con el principal autenticado. Consecuencia: el usuario del perfildevdebe pasar a ser una de esas cuentas, y los.httpdewiki/httpcambian con él.Esa búsqueda por
nombre_accesoes provisional. Por ADR-0009 la identidad la emite un proveedor externo, y la clave natural de una identidad externa es(proveedor_identidad, identificador_externo)—el issuer y el subject del token—, que es la restricción única global del esquema, mientrasnombre_accesosolo es único por organización. El cambio de clave llega con el resource server en M8. -
Aguas abajo: todos los slices de M1. S1.02 (consecutivos) depende del reloj y de DR-03.
-
Adyacente: ADR-0008 (arquitectura de errores) fija los códigos HTTP de los rechazos de AC-002 y AC-003. Este documento no los prejuzga.
8. Decisiones requeridas y preguntas abiertas
Ninguna se resuelve en silencio, según MARCO.md parte VII.
DR-01 · ¿Qué es «organización» en el dominio?
No está en el lenguaje ubicuo (§2.1) ni en las 28 reglas consolidadas (§16), pero es not null en
26 de las 90 tablas del esquema, cuenta_usuario pertenece a una, y el DDL creó unique (id, organizacion_id) en
varias tablas con el único propósito de que las claves ajenas compuestas impidan cruzarla.
NO ERA UNA DECISIÓN ABIERTA: estaba documentada, y no la encontré. El comentario de la tabla la
define como «raíz de aislamiento de una instalación o entidad legal que opera el LIS», tanto en
docs/LIS_modelo_datos_postgresql_18.sql como en V1__esquema_base.sql línea 51. La organización es
el tenant. Se registra el error de búsqueda porque explica el guardarraíl que se añadió a
00-agents.md: se buscó en el marco conceptual y en las definiciones de tabla, pero no en los
comentarios del esquema, que es donde el modelo de datos guarda sus definiciones.
Cómo se garantiza, y en qué orden. Dos mecanismos, y se eligen los dos, pero no a la vez:
- Ahora, en S1.01: la organización explícita en la firma de toda consulta. Es la parte cuyo coste crece con cada repositorio que se escribe, y quedan 22 slices.
- Después, en su propio ADR: RLS como red de seguridad. Su coste es plano —una política por
tabla, independiente del número de repositorios—, así que no urge. Y hoy sería inerte: no hay
rol de aplicación y la app conecta como
postgres, que es superusuario y dueño de las tablas; ambos saltan RLS salvoforce row level security. Adoptarlo exige antes un rol sin privilegios de dueño, lo que es una migración y un cambio en cómo se autentica la aplicación.
Con RLS de red la firma sigue siendo necesaria: el caso de uso debe ser honesto sobre su alcance aunque la base lo respalde. Por eso el orden es este y no el inverso.
DR-02 · ¿De dónde sale la sede del actor?
cuenta_usuario no tiene sede y cuenta_rol tampoco. ADR-0005
anticipaba que «un empleado accede a los de su sede», que con el esquema actual no es expresable.
Opciones: la sede es un dato de la petición, es una selección explícita del operador, o falta
soporte en el esquema y la restricción por sede es de M8.
Bloquea: la parte «sede» de S1.01 y el filtrado por sede de la lista de trabajo (S1.12). No bloquea: US-001, US-002, US-003 ni US-004.
DR-03 · ¿Qué zona horaria gobierna la fecha civil?
organizacion.zona_horaria y sede.zona_horaria existen las dos, y el DDL no dice cuál manda.
RN-18 exige consecutivos «sin huecos por año» y RN-20 distingue instante de fecha civil: el año y
el día de qué zona, si una organización tiene sedes en husos distintos.
El esquema resuelve la mitad. consecutivo_documento tiene clave primaria
(organizacion_id, tipo_documento, anio), sin sede: el contador es de la organización, así que
el año que lo particiona tiene que ser el de la organización. Dos sedes en husos distintos
compartiendo un contador y discrepando sobre el año en la noche del 31 de diciembre es precisamente
el hueco que RN-18 prohíbe. Se registra como deducción del modelo, no como decisión de
implementación.
Y deja una trampa preparada. La firma es
siguiente_numero_documento(p_organizacion_id uuid, p_tipo_documento text, p_fecha date default current_date).
Ese current_date se evalúa en la zona de la sesión de PostgreSQL, que es un tercer huso ajeno
a los dos anteriores. S1.02 debe pasar la fecha explícitamente, calculada con el reloj y la zona
de la organización; confiar en el valor por omisión hace que el año del consecutivo dependa de cómo
esté configurado el servidor de base de datos.
Lo que sigue abierto es la fecha civil operativa: el corte del día para una lista de trabajo, un turno o la vigencia de estabilidad de una muestra. Ahí la zona de la sede es defendible, porque el hecho físico ocurre en la sede. No lo decide el esquema.
ASSUMPTION mientras no exista el caso: hoy hay una sola organización sembrada y sus sedes
comparten huso (America/Bogota), así que la pregunta es teórica. El reloj recibe una zona explícita
desde el primer día —nunca la del proceso— pero solo hay un valor posible. Se revisa cuando aparezca
una sede en otro huso, y el supuesto queda escrito para que ese día se note.
Ya no bloquea nada: ni el año de S1.02, que queda deducido, ni AC-004, que se construye sobre el supuesto declarado.
OQ-01 · ¿Cómo se hace verificable el alcance?
Una firma que exige la organización lo hace imposible de olvidar, pero no impide pasar la
equivocada. Una regla de ArchUnit puede exigir que los métodos de repositorio la reciban, pero no
que se use en el where. Un test por repositorio sí lo comprueba, pero hay que acordarse de
escribirlo. Se decide al construir el slice, con las tres opciones sobre la mesa.
OQ-02 · ¿Qué significa una organización inactiva?
organizacion.activo existe. Ninguna regla dice si sus cuentas pueden seguir operando.
9. Trazabilidad
| ID | Tipo | Descripción | Prioridad | Estado |
|---|---|---|---|---|
| US-001 | User Story | Identificadores tipados | Must | Construida |
| US-002 | User Story | Resolución del actor y su organización | Must | Construida |
| US-003 | User Story | Alcance por organización en toda lectura | Must | Construida para sedes |
| US-004 | User Story | Reloj inyectable | Must | Construida |
| BR-01..BR-09 | Reglas | Ver §2 | Must | Aprobadas (de RN o del modelo) |
| DR-01 | Decisión | Naturaleza de «organización» | Must | Cerrada: ya estaba definida en el esquema |
| DR-02 | Decisión | Origen de la sede del actor | Should | Abierta |
| DR-03 | Decisión | Zona horaria de la fecha civil operativa | Should | Supuesto declarado: huso único hoy |
| OQ-01 | Pregunta | Verificabilidad del alcance | Must | Resuelta: firma + test por repositorio |
| OQ-02 | Pregunta | Organización inactiva | Could | Abierta |
Cadena de trazabilidad, según PLAN_MAESTRO.md §6:
RN-02, RN-20 · modelo de datos → CAP-01 → US-00x → AC-00x → S1.01 → test
10. Revisión
Pendiente de revisión adversarial. DR-01 resuelta —la organización es el tenant— y DR-03 reducida a un supuesto declarado, así que US-001 a US-004 quedan listas para construirse. DR-02 sigue abierta y solo afecta a la parte «sede», que no entra en este slice.
Aprobado para construir: AC-001 a AC-004, con los códigos HTTP de sus rechazos pendientes de ADR-0008 y el comportamiento ya fijado aquí.
Construido y verificado (2026-09-25)
AC-001 a AC-004 implementados. Los cuatro tests de la API se vieron fallar antes de pasar, y el de aislamiento falló devolviendo la sede de la otra organización: la fuga era real, no hipotética.
| Criterio | Dónde se verifica |
|---|---|
| AC-001 | ContextoEjecucionTest — los tres identificadores rechazan el nulo al construirse |
| AC-002 | ResolverContextoTest y SedeApiTest — sin cuenta y cuenta no operativa, ambos rechazados |
| AC-003 | ListarSedesTest (sin base) y SedeApiTest (con dos organizaciones en PostgreSQL real) |
| AC-004 | RelojTest — el mismo instante es día 25 en Bogotá y 26 en Tokio |
OQ-01 resuelta por la práctica: la firma exige la organización, así que olvidarla no compila; el
fake en memoria filtra de verdad, así que un caso de uso que no la propague falla sin base de datos;
y SedeApiTest siembra dos organizaciones, porque con un solo inquilino un repositorio que no filtra
pasa igual. Las tres cosas juntas, no una.
Añadido a ArquitecturaTest: la regla «el dominio no loguea» que
ADR-0007 prometió. Son seis reglas ahora, y se
comprobó metiendo un LoggerFactory en CuentaUsuario a propósito.
Lo que este slice no cubre: la parte «sede» del contexto, que sigue bloqueada por DR-02, y el
contrato de errores, que ADR-0008 fijará. El rechazo devuelve hoy un 403 con cuerpo vacío desde un
@ExceptionHandler local al controlador, deliberadamente no generalizado.