Requisitos por capacidad

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.sql siembra la organización LAB-DEMO (zona America/Bogota) y las cuentas admin.demo y bio.demo, así que el actor se resuelve buscando la cuenta_usuario cuyo nombre_acceso coincide con el principal autenticado. Consecuencia: el usuario del perfil dev debe pasar a ser una de esas cuentas, y los .http de wiki/http cambian con él.

    Esa búsqueda por nombre_acceso es 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, mientras nombre_acceso solo 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:

  1. 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.
  2. 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 salvo force 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.