diff --git a/.env.example b/.env.example
index bc13a16..bf0d769 100644
--- a/.env.example
+++ b/.env.example
@@ -4,9 +4,32 @@
DB_URL=jdbc:postgresql://server02:30432/orquestador
DB_USERNAME=pgadmin
DB_PASSWORD=
+# Para separar migraciones y runtime, configurar un rol propietario distinto:
+# DB_MIGRATION_USERNAME=orchestrator_migrator
+# DB_MIGRATION_PASSWORD=
SPRING_PROFILES_ACTIVE=postgres96
JWT_ISSUER_URI=http://keycloakic.ddns.net/auth/realms/controlms
JWT_JWK_SET_URI=http://keycloakic.ddns.net/auth/realms/controlms/protocol/openid-connect/certs
JWT_AUDIENCE=orquestador-nmp
APP_BIND_ADDRESS=127.0.0.1
APP_PORT=8080
+
+# T07: claim de negocio obligatorio para JWT autenticados; no es el scope OAuth.
+JWT_BUSINESS_SCOPE_CLAIM=business_scope
+# Habilitar solo con DB_USERNAME restringido y DB_MIGRATION_USERNAME separado.
+# pgadmin no puede usarse como runtime cuando la administracion esta habilitada.
+FLOW_ADMIN_ENABLED=false
+# API de solicitudes independiente de administracion; exige el mismo runtime DB restringido.
+REQUEST_API_ENABLED=false
+WORKER_ENABLED=false
+INTEGRATION_ROUTES_PATH=/app/config/integration-routes.json
+WORKER_CONCURRENCY=10
+WORKER_BATCH=10
+WORKER_POLL_MS=500
+WORKER_LEASE_SECONDS=60
+WORKER_RENEW_SECONDS=15
+WORKER_SHUTDOWN_SECONDS=20
+# Lista de ambitos autorizados para administrar el catalogo global; vacia deniega todo.
+ADMIN_BUSINESS_SCOPES=
+# Ruta dentro del contenedor; compose monta ./config como solo lectura.
+FLOW_CATALOG_PATH=/app/config/publication-catalog.json
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 50536eb..3b14af6 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -7,6 +7,7 @@ permissions:
jobs:
verify:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
@@ -14,8 +15,14 @@ jobs:
distribution: temurin
java-version: '21'
cache: maven
- - name: Verify
+ - name: Docker available for Testcontainers
+ run: docker info
+ - name: Verify acceptance checker
+ run: python3 -m unittest discover -s scripts -p 'test_*.py'
+ - name: Verify with isolated PostgreSQL Testcontainers
run: sh ./mvnw --batch-mode --no-transfer-progress clean verify
+ - name: Require A01-A20 without skipped tests
+ run: python3 scripts/verify_acceptance.py --require-testcontainers
- name: Test reports
if: always()
uses: actions/upload-artifact@v4
@@ -24,4 +31,7 @@ jobs:
path: |
target/surefire-reports/
target/failsafe-reports/
- if-no-files-found: ignore
+ target/acceptance-report.json
+ specs/acceptance-cases.json
+ if-no-files-found: error
+ retention-days: 30
diff --git a/.gitignore b/.gitignore
index 89784a3..573c041 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
/.env
*.log
/.tools/
+__pycache__/
diff --git a/README.md b/README.md
index c80852c..544597b 100644
--- a/README.md
+++ b/README.md
@@ -30,10 +30,11 @@ Salud: `http://localhost:8080/actuator/health/readiness`. El puerto publicado se
- [Arquitectura](specs/ARCHITECTURE.md): paquetes y fronteras.
- [Decisiones](specs/DECISIONS.md): versiones y motivos.
+- [Administración interna T06](specs/FLOW_PUBLICATION.md): validación, publicación, catálogo y transacciones; REST seguro pendiente de T07.
- [Plan T01–T17](specs/IMPLEMENTATION_PLAN.md): dependencias, subtareas y criterios de cierre.
- [Seguimiento](specs/PROGRESS.md): estado real, verificaciones y siguiente paso.
-Surefire ejecuta `*Test` en `test`; Failsafe ejecuta `*IT` en `verify`. T01 incorpora un smoke de despliegue con cinco comprobaciones; T03 incorporará Testcontainers. CI ejecuta `clean verify` sin acceder a servidores privados. Para verificar explícitamente un servicio iniciado, usar JDK 21 y ejecutar:
+Surefire ejecuta `*Test` en `test`; Failsafe ejecuta `*IT` en `verify`. T01 incorpora un smoke de despliegue con cinco comprobaciones; T03 incorpora Testcontainers PostgreSQL 9.6.24 y requiere Docker para verify. CI ejecuta `clean verify` sin acceder a servidores privados. Para verificar explícitamente un servicio iniciado, usar JDK 21 y ejecutar:
```powershell
.\mvnw.cmd clean verify -Pdeployment-smoke '-Dsmoke.baseUrl=http://server02:8787'
@@ -41,10 +42,104 @@ Surefire ejecuta `*Test` en `test`; Failsafe ejecuta `*IT` en `verify`. T01 inco
Este perfil comprueba salud, readiness, liveness y rechazo de solicitudes sin token/con token malformado. No contiene todavía pruebas de negocio ni de JWT válido.
-`clean verify` valida los contratos y sus ejemplos sin servicios externos. Consultar [convenciones REST](contracts/README.md), [semántica del DSL](schemas/README.md) y [fixtures T02](examples/contracts/README.md). Los ejemplos de `examples/http/contracts.http` son contratos objetivo, no endpoints implementados. Los flujos de crédito completos corresponden a T14.
+`test` valida los contratos y sus ejemplos sin servicios externos; `clean verify` agrega migraciones e integración PostgreSQL. Sin Docker local, usar el perfil migration-tests-external descrito en RUNBOOK. Consultar [convenciones REST](contracts/README.md), [semántica del DSL](schemas/README.md) y [fixtures T02](examples/contracts/README.md). Los ejemplos de `examples/http/contracts.http` son contratos objetivo, no endpoints implementados. Los flujos de crédito completos corresponden a T14.
-El Dockerfile incluye contratos/schemas/fixtures en la etapa de construcción para ejecutar las mismas pruebas y ejecuta con JRE 21 como usuario no root. Compose no administra PostgreSQL ni sus volúmenes. Migraciones, mocks y roles de mínimo privilegio siguen pendientes de T03/T11/T07.
+El Dockerfile incluye contratos/schemas/fixtures en la etapa de construcción para ejecutar las mismas pruebas y ejecuta con JRE 21 como usuario no root. Compose no administra PostgreSQL ni sus volúmenes. T03 incluye migraciones y permisos PostgreSQL; mocks y autorización funcional siguen pendientes de T11/T07.
## Límites funcionales previstos
Nuevos umbrales, destinos y pasos con capacidades existentes se publicarán como configuración. Una nueva capacidad de validación puede requerir un adapter; un nuevo tipo de tarea puede requerir soporte del front. No incluye UI administrativa, Kafka, Drools ni migración automática de instancias.
+
+## Seguridad y administración (T07)
+
+La administración REST requiere habilitación explícita y credenciales PostgreSQL
+separadas de migración/ejecución. Ver [SECURITY.md](specs/SECURITY.md) y las variables
+nuevas de `.env.example`. Compose conserva únicamente el servicio y monta `config/`
+como solo lectura; PostgreSQL sigue externo. No se ha desplegado esta versión.
+
+## Solicitudes (T08)
+
+Implementados POST y GET de solicitudes, con versión fijada, idempotencia durable,
+validación de datos y autorización. Habilitar REQUEST_API_ENABLED=true únicamente
+con runtime DB restringido; administración puede permanecer apagada.
+[Configuración y semántica](specs/REQUESTS.md); [ejemplo HTTP](examples/http/requests.http).
+No se ha desplegado esta versión. Eventos y worker continúan pendientes.
+
+## Eventos (T09)
+
+POST /requests/{id}/events acepta comandos idempotentes y retorna 202/Location.
+Ejecución PENDING, revisión y auditoría se confirman juntas. Ver
+[EVENTS.md](specs/EVENTS.md) y [ejemplo](examples/http/events.http).
+El worker y GET de ejecución continúan pendientes en T10: 202 no indica que se
+haya procesado la acción. No se desplegó esta versión.
+
+## Worker y consulta de ejecución (T10)
+
+GET /api/v1/executions/{id} ya permite seguimiento autorizado. WORKER_ENABLED=true
+activa procesamiento durable con leases y checkpoints; defaults y límites en
+[WORKER.md](specs/WORKER.md). V3 conserva clasificación de efectos de acciones.
+Los adapters T11 requieren contratos y rutas configurados; los registros por
+defecto están vacíos. Flujos sin acciones pueden concluir.
+No se ha desplegado esta versión ni se han invocado proveedores reales.
+
+## Integraciones (T11)
+
+INVOKE_INTEGRATION ejecuta POST JSON con contratos/perfiles fijados y límites de
+tiempo y tamaño. REGISTER_DOCUMENT verifica metadatos y registra referencias de
+forma atómica. NO_OP se limita a simulación.
+[Configuración y límites](specs/INTEGRATIONS.md);
+[ejemplos de contratos](examples/integrations/publication-catalog.json) y
+[rutas](examples/integrations/integration-routes.example.json).
+Los ejemplos usan dominios .invalid y datos sintéticos; requieren adaptación al
+proveedor real. Compose sigue levantando únicamente el servicio con PostgreSQL
+externo.
+
+## Reintentos y conciliación (T12)
+
+El worker programa reintentos durables para fallos transitorios seguros, con
+backoff, jitter y Retry-After acotados. Los efectos inciertos mantienen bloqueada
+la solicitud hasta una resolución con evidencia por EXECUTION_OPERATOR.
+[Operación y límites](specs/RECONCILIATION.md);
+[ejemplos HTTP](examples/http/reconciliation.http).
+La resolución requiere administración habilitada y ámbito autorizado; no permite
+elegir manualmente el estado de negocio. La evidencia es verificada por el operador,
+sin consulta automática a un proveedor real.
+
+## Eventos disponibles y documentos (T13)
+
+GET /api/v1/requests/{id}/available-events devuelve eventos, schemas y metadatos
+FORM/DOCUMENT_UPLOAD de la versión fijada; oculta tareas con ejecución activa.
+No incluye desarrollar una interfaz gráfica. El worker verifica también los
+límites documentales de la tarea publicada.
+[Semántica y pruebas](specs/AVAILABLE_EVENTS.md);
+[ejemplo HTTP](examples/http/available-events.http).
+
+## Flujos de demostración (T14)
+
+[Crédito v1/v2 y compra](examples/flows/README.md) incluyen definiciones completas,
+catálogo y ejemplos HTTP. DemoFlowsIT publica y recorre los archivos exactos usando
+API, worker, PostgreSQL de pruebas y proveedores HTTP simulados. La v2 incorpora
+licencia por configuración; las solicitudes existentes conservan v1. No hay front
+ni cobro real. La integración de proveedores reales requiere validación separada.
+
+## Auditoría y observabilidad (T15)
+
+GET /api/v1/requests/{id}/history consulta el historial del propietario y ámbito
+con filtros y cursor. Actuator metrics requiere EXECUTION_OPERATOR y un ámbito
+incluido en ADMIN_BUSINESS_SCOPES. Incluye estados de cola, edad del pendiente,
+leases vencidos, resultados HTTP y latencia de integraciones sin etiquetas por UUID.
+[Operación y límites](specs/OBSERVABILITY.md);
+[ejemplos HTTP](examples/http/history-operations.http).
+WORKER_SHUTDOWN_SECONDS controla el drenaje (1–20 s, predeterminado 20).
+
+## Verificación integral y simulación (T16)
+
+[Guía y matriz ejecutable A01–A20](specs/ACCEPTANCE.md). Después de clean verify,
+`python scripts/verify_acceptance.py --require-testcontainers` exige evidencia de
+cada caso y ningún test omitido. El perfil externo identifica explícitamente su
+entorno y no pasa esa puerta Docker. CI archiva reportes y matriz.
+
+POST /api/v1/admin/flows/{id}/simulate evalúa fixtures con FLOW_EDITOR y ámbito
+administrativo autorizado. Usa la proyección compartida con el worker, sin proveedores
+ni escrituras de negocio. [Semántica](specs/SIMULATION.md) y
+[ejemplo HTTP](examples/http/simulate.http).
diff --git a/compose.yaml b/compose.yaml
index 1f7fc09..b663987 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -9,12 +9,29 @@ services:
DB_URL: ${DB_URL:?Configura DB_URL en .env}
DB_USERNAME: ${DB_USERNAME:?Configura DB_USERNAME en .env}
DB_PASSWORD: ${DB_PASSWORD:?Configura DB_PASSWORD en .env}
+ DB_MIGRATION_USERNAME: ${DB_MIGRATION_USERNAME:-${DB_USERNAME}}
+ DB_MIGRATION_PASSWORD: ${DB_MIGRATION_PASSWORD:-${DB_PASSWORD}}
JWT_ISSUER_URI: ${JWT_ISSUER_URI:?Configura JWT_ISSUER_URI en .env}
JWT_JWK_SET_URI: ${JWT_JWK_SET_URI:?Configura JWT_JWK_SET_URI en .env}
JWT_AUDIENCE: ${JWT_AUDIENCE:?Configura JWT_AUDIENCE en .env}
+ JWT_BUSINESS_SCOPE_CLAIM: ${JWT_BUSINESS_SCOPE_CLAIM:-business_scope}
+ FLOW_ADMIN_ENABLED: ${FLOW_ADMIN_ENABLED:-false}
+ REQUEST_API_ENABLED: ${REQUEST_API_ENABLED:-false}
+ WORKER_ENABLED: ${WORKER_ENABLED:-false}
+ INTEGRATION_ROUTES_PATH: ${INTEGRATION_ROUTES_PATH:-/app/config/integration-routes.json}
+ WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-10}
+ WORKER_BATCH: ${WORKER_BATCH:-10}
+ WORKER_POLL_MS: ${WORKER_POLL_MS:-500}
+ WORKER_LEASE_SECONDS: ${WORKER_LEASE_SECONDS:-60}
+ WORKER_RENEW_SECONDS: ${WORKER_RENEW_SECONDS:-15}
+ WORKER_SHUTDOWN_SECONDS: ${WORKER_SHUTDOWN_SECONDS:-20}
+ ADMIN_BUSINESS_SCOPES: ${ADMIN_BUSINESS_SCOPES:-}
+ FLOW_CATALOG_PATH: ${FLOW_CATALOG_PATH:-/app/config/publication-catalog.json}
SERVER_PORT: "8080"
ports:
- "${APP_BIND_ADDRESS:-127.0.0.1}:${APP_PORT:-8080}:8080"
extra_hosts:
- "host.docker.internal:host-gateway"
+ volumes:
+ - ./config:/app/config:ro
stop_grace_period: 40s
diff --git a/config/README.md b/config/README.md
new file mode 100644
index 0000000..329e420
--- /dev/null
+++ b/config/README.md
@@ -0,0 +1,12 @@
+# Catálogo confiable
+
+publication-catalog.json configura la publicación T06/T07. Compose monta este
+directorio en /app/config con permisos de solo lectura. El ejemplo permite roles
+ANALISTA y no registra capacidades externas. No contiene secretos ni mocks.
+
+Campos raíz obligatorios: roles (array), handlers, integrations y documents (objetos).
+PublicationCatalogLoader valida campos exactos y rechaza claves duplicadas.
+Las estructuras tipadas y las reglas de publicación se describen en
+../specs/FLOW_PUBLICATION.md. El registro de capacidades y contratos de proveedor
+debe acompañar a handlers reales en T11; declararlos aquí no implementa ejecución.
+Para habilitar las rutas administrativas ver ../specs/SECURITY.md.
diff --git a/config/integration-routes.json b/config/integration-routes.json
new file mode 100644
index 0000000..60cd85d
--- /dev/null
+++ b/config/integration-routes.json
@@ -0,0 +1 @@
+{"routes":[]}
diff --git a/config/publication-catalog.json b/config/publication-catalog.json
new file mode 100644
index 0000000..2457a69
--- /dev/null
+++ b/config/publication-catalog.json
@@ -0,0 +1,6 @@
+{
+ "roles": ["ANALISTA"],
+ "handlers": {},
+ "integrations": {},
+ "documents": {}
+}
diff --git a/contracts/README.md b/contracts/README.md
index 40380cc..bcc1641 100644
--- a/contracts/README.md
+++ b/contracts/README.md
@@ -32,3 +32,30 @@ Conciliación sólo permite RESUME con resultados verificados por acción o CLOS
Con JDK 21: `./mvnw clean verify` (Windows: `.\mvnw.cmd clean verify`). ContractValidationTest comprueba el schema oficial OpenAPI 3.1, parseo Swagger, resolución local de referencias, meta-schema 2020-12, fixtures positivos/negativos, ejemplos embebidos y cabeceras/rutas obligatorias. Los validadores tienen scope test; no agregan dependencias Jackson 2 al runtime.
Swagger Parser 2.1.35 se usa sin expansión recursiva (`resolve=false`): expandir el AST produjo un error de profundidad. Las referencias se verifican explícitamente y networknt valida los ejemplos con los schemas reales; no se ignoran referencias rotas. Las pruebas normales no contactan server02, PostgreSQL, Keycloak ni proveedores. No sustituyen T05/T06/T07/T16.
+
+## Estado de implementación T07
+
+Crear, editar, validar, publicar, activar y retirar flujos tienen controlador REST
+protegido. Simulación continúa pendiente (T14), conciliación en T12 y rutas de
+negocio en T08–T15. Configuración, permisos DB y política por recurso:
+[SECURITY.md](../specs/SECURITY.md). SecurityHttpIT verifica JWT RSA mediante JWKS
+local, HTTP real y PostgreSQL aislado; no valida aún la configuración real de controlms.
+
+Desde T06 networknt y Jackson 2 se usan también en infraestructura de runtime para
+validar DSL; Swagger Parser permanece solo en test. Esto sustituye la descripción
+histórica de todos los validadores como dependencias exclusivamente de test.
+
+T08 implementa POST /requests y GET /requests/{id}, con JWT, propiedad/ámbito y
+ETag. Creación repetida retorna snapshot original; consulta retorna estado actual.
+Tipo inexistente 404; no asignable/cambio concurrente 409. Ver
+[REQUESTS.md](../specs/REQUESTS.md) para habilitación y protocolo transaccional.
+
+T09 implementa POST /requests/{id}/events con 202/Location, bloqueo de solicitud,
+replay antes de revisión y persistencia atómica. Ver EVENTS.md en specs.
+GET /executions/{id} sigue como contrato objetivo y se implementará en T10;
+no se debe interpretar el 202 como procesamiento completado.
+
+T10 implementa GET /executions/{id} con autorización por recurso y ExecutionView,
+y worker opcional. Sustituye el estado pendiente descrito en T09. 202 sigue siendo
+aceptación; consultar ejecución para resultado y luego refrescar solicitud/ETag.
+Ver specs/WORKER.md para efectos, checkpoints, límites y configuración.
diff --git a/contracts/openapi.yaml b/contracts/openapi.yaml
index 16d90b5..5ac37ed 100644
--- a/contracts/openapi.yaml
+++ b/contracts/openapi.yaml
@@ -3,7 +3,7 @@ jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
info:
title: Orquestador NMP
version: 1.0.0
- description: Contrato objetivo del MVP. Las rutas de negocio aún no están implementadas. JWT y autorización por
+ description: Contrato objetivo del MVP. Solicitudes, eventos, ejecuciones, conciliación, tareas e historial implementados hasta T15; simulación administrativa implementada en T16. JWT y autorización por
recurso obligatorios. Campos opcionales ausentes se omiten; no se usa null salvo en datos configurables.
servers:
- url: /api/v1
@@ -18,9 +18,9 @@ paths:
operationId: createRequest
summary: Crear solicitud y fijar flujo activo
description: 'Owner y businessScope se derivan del JWT autorizado. Idempotencia durable por actor/scope: repetición
- idéntica retorna la misma solicitud (201); otro hash retorna 409. data se valida con initialDataSchema y
+ idéntica retorna la respuesta original de creación (201, Location y ETag originales), incluso si cambió el flujo activo o avanzó la solicitud; otro hash retorna 409. GET devuelve el estado actual. data se valida con initialDataSchema y
dataSchema del flujo. Máximo de datos 256 KiB. No se aceptan estado, propietario o resultados de proveedor
- como autoridad del cliente.'
+ como autoridad del cliente. Tipo inexistente retorna 404; tipo deshabilitado, sin flujo publicado activo o cambiado durante la asignación retorna 409 INVALID_FLOW_STATE. Un 409 por cambio concurrente permite reintentar la misma clave, que no queda reservada.'
tags:
- Requests
parameters:
@@ -63,6 +63,8 @@ paths:
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'413':
@@ -133,7 +135,7 @@ paths:
operationId: availableEvents
summary: Consultar tareas y eventos permitidos
description: 'Orientativo: el POST revalida roles y guardias. Con ejecución activa, events vacío y blockedByExecutionId
- presente. Sólo se exponen causas de bloqueo no sensibles.'
+ presente. Sólo se exponen causas de bloqueo no sensibles. Filtra por propietario, ámbito y roles; usa la versión fijada incluso retirada. Guardias dependientes de event.data se difieren al POST. Cache-Control no-store, sin ETag; revision sirve para If-Match del evento.'
tags:
- Requests
parameters:
@@ -168,7 +170,7 @@ paths:
description: 'Una transición por evento; sin HTTP externo en la transacción de aceptación. El hash incluye
evento, datos y revisión esperada canónicos. Primero comprobar acceso e idempotencia y después revisión;
un replay idéntico devuelve el mismo executionId/status actual incluso si cambió revision. Nueva clave mientras
- existe ejecución activa: REQUEST_BUSY. Validar data con schema del evento. 202 no equivale a éxito de negocio.'
+ existe ejecución activa: REQUEST_BUSY (se comprueba revisión antes de busy). Validar data con schema del evento. Reintentar conservando If-Match original. 202 no equivale a éxito de negocio; GET de ejecución permite seguimiento; el worker se habilita explícitamente con WORKER_ENABLED.'
tags:
- Requests
parameters:
@@ -268,7 +270,7 @@ paths:
operationId: getHistory
summary: Consultar auditoría paginada y filtrada
description: Orden ascendente (createdAt,id), cursor opaco asociado a solicitud y filtros. Máximo 100. Omitir
- nextCursor al terminar. from inclusivo y to exclusivo; actor/detalle filtrados según autorización.
+ nextCursor al terminar. from inclusivo y to exclusivo, UTC Z. Propietario y ámbito obligatorios, sin bypass administrativo. Detail se proyecta por lista permitida (IDs de reglas/acciones, razones y códigos); no entrega evidencia ni resultados completos. Cache-Control no-store. Cursor representa posición, no autorización; no promete snapshot entre páginas concurrentes.
tags:
- Requests
parameters:
@@ -828,7 +830,7 @@ paths:
summary: Simular evaluación pura
description: Sin HTTP ni efectos persistentes. requestData/actor/results son fixtures administrativos, nunca
comandos de negocio. Evaluar guardia/decisión/updates con resultados suministrados; rechazo de tipos y ambigüedad
- como problem 422. NO_OP sólo permitido en simulación.
+ como problem 422. Body máximo 1 MiB; requestData/event.data/cada resultado máximo 256 KiB. results usa exactamente los resultKey externos de la transición. NO_OP sólo permitido en simulación, toma output de la definición sin fixture externo. Cache-Control no-store. Actor de fixture no sustituye JWT FLOW_EDITOR ni ámbito administrativo.
tags:
- Administration
parameters:
@@ -1236,8 +1238,8 @@ paths:
summary: Conciliar una ejecución incierta
description: Sólo RECONCILIATION_REQUIRED, con acceso por scope. Evidencia referenciada y verificada por operador
autorizado; no admite elegir estado destino. RESUME registra resultados verificados y reencola PENDING;
- CLOSE_WITHOUT_TRANSITION verifica ausencia/compensación de efectos y cierra FAILED conservando negocio.
- Idempotencia durable por ejecución/clave y comando canónico; replay idéntico devuelve misma resolución.
+ CLOSE_WITHOUT_TRANSITION registra ausencia/compensación verificada por el operador y cierra FAILED conservando negocio. No consulta automáticamente al proveedor.
+ Idempotencia durable por ejecución/clave y comando canónico; replay exige el mismo sujeto operador y devuelve la misma resolución.
Auditoría obligatoria.
tags:
- Administration
@@ -1266,6 +1268,8 @@ paths:
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
+ '412':
+ $ref: '#/components/responses/PreconditionFailed'
'413':
$ref: '#/components/responses/PayloadTooLarge'
'422':
diff --git a/examples/flows/README.md b/examples/flows/README.md
index cb32445..a6d688b 100644
--- a/examples/flows/README.md
+++ b/examples/flows/README.md
@@ -1,5 +1,53 @@
-# Flujos pendientes de T14
+# Flujos demostrativos T14
-Entregar credito-v1.json, credito-v2-licencia.json y compra-v1.json completos y publicables.
-La versión 2 incorpora licencia antes del buró utilizando capacidades existentes.
-No presentar fragmentos como flujos funcionales.
+| Archivo | Recorrido |
+| --- | --- |
+| credito-v1.json | Cliente → INE → estado de cuenta → buró → PASO_N/PASO_M/REVISION_MANUAL |
+| credito-v2-licencia.json | Mismo recorrido; agrega licencia antes del buró |
+| compra-v1.json | Cliente → confirmación → COMPRA_CONFIRMADA/COMPRA_CANCELADA |
+
+Compra demuestra un segundo tipo; no cobra ni reserva inventario. Score >70 lleva
+a PASO_N; >60 y <=70 a PASO_M; <=60 a REVISION_MANUAL. Score ausente/no numérico
+es fallo técnico. INVALID documental conserva tarea y reasonCodes; cliente ausente
+conserva CREADA.
+
+Usar el [catálogo completo](../integrations/publication-catalog.json). Los perfiles
+INE, ESTADO_CUENTA y LICENCIA_CONDUCIR de DOCUMENTO_VALIDA están fijados en versión 1.
+Máximo documental: 5 MiB; estado de cuenta solo PDF; INE/licencia PDF, JPEG o PNG.
+El API recibe documentId; verifica metadata del proveedor, no recibe archivos.
+consentimientoId es obligatorio; la validez real debe verificarse con el proveedor.
+
+## Prueba reproducible
+
+Con JDK 21 y MIGRATION_TEST_DB_URL/USER/PASSWORD apuntando a una base de pruebas:
+
+```powershell
+./mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external '-Dtest=DemoFlowTest' '-Dit.test=DemoFlowsIT'
+```
+
+Inicia API con JWT/JWKS local, publica los JSON exactos, activa ambos tipos, crea
+solicitudes y ejecuta eventos/worker/SQL. Cuatro proveedores HTTP loopback exclusivos
+de test devuelven datos sintéticos. Crea y elimina esquemas/roles temporales.
+Cubre coexistencia v1/v2, licencia inválida/válida, score 71/70/61/60, cliente ausente,
+score mal formado y ambos finales de compra. No requiere Keycloak ni buró real.
+
+## Recorrido manual en entorno de demo
+
+1. Configurar FLOW_CATALOG_PATH con el catálogo de ejemplo, FLOW_ADMIN_ENABLED,
+ REQUEST_API_ENABLED y WORKER_ENABLED, JWT y ADMIN_BUSINESS_SCOPES según README.
+ Usar usuario DB runtime restringido y rutas de proveedores de demo confiables.
+ integration-routes.example.json contiene hosts .invalid, no servicios operativos.
+2. Crear tipos con [seed-request-types.sql](seed-request-types.sql) como dueño del
+ esquema. Ajustar el esquema al configurado; no es una migración automática.
+3. [demo-flows.http](../http/demo-flows.http) contiene publicación, activación y
+ recorridos. Copiar IDs/ETags de respuestas y proporcionar tokens de cada rol.
+4. Crear crédito con v1, activar v2 y crear otro. Cada solicitud conserva su flowId;
+ después del estado de cuenta solo la nueva exige REGISTRAR_LICENCIA.
+5. Para cada evento usar If-Match de la consulta actual. Esperar SUCCEEDED antes de
+ continuar: 202 solo significa aceptado. Reintento de transporte conserva clave;
+ nueva operación de negocio usa una clave nueva. DocumentId debe existir en el
+ proveedor documental de demo y pertenecer a la solicitud/ámbito correspondiente.
+
+No invoca /simulate: recorre la API operativa contra mocks. No modifica Java de
+producción, migraciones ni columnas. Proveedores reales pendientes de contrato y
+verificación independiente. Docker permanece con un servicio y PostgreSQL externo.
diff --git a/examples/flows/compra-v1.json b/examples/flows/compra-v1.json
new file mode 100644
index 0000000..4aa42d3
--- /dev/null
+++ b/examples/flows/compra-v1.json
@@ -0,0 +1,272 @@
+{
+ "schemaVersion": 1,
+ "flowKey": "COMPRA",
+ "version": 1,
+ "initialState": "CREADA",
+ "initialDataSchema": {
+ "type": "object",
+ "properties": {
+ "client": {
+ "type": "string",
+ "minLength": 1
+ },
+ "orderId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "amount": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ }
+ },
+ "required": [
+ "client",
+ "orderId",
+ "amount"
+ ],
+ "additionalProperties": false
+ },
+ "dataSchema": {
+ "type": "object",
+ "properties": {
+ "client": {
+ "type": "string",
+ "minLength": 1
+ },
+ "orderId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "amount": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "cliente": {
+ "type": "object",
+ "properties": {
+ "found": {
+ "type": "boolean"
+ },
+ "customerId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "found"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "found": {
+ "const": true
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "customerId"
+ ]
+ }
+ }
+ ]
+ },
+ "confirmada": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "client",
+ "orderId",
+ "amount"
+ ],
+ "additionalProperties": false
+ },
+ "states": [
+ {
+ "key": "CREADA",
+ "terminal": false
+ },
+ {
+ "key": "CONFIRMACION_PENDIENTE",
+ "terminal": false
+ },
+ {
+ "key": "COMPRA_CONFIRMADA",
+ "terminal": true
+ },
+ {
+ "key": "COMPRA_CANCELADA",
+ "terminal": true
+ }
+ ],
+ "events": [
+ {
+ "key": "ASIGNAR_CLIENTE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ }
+ },
+ {
+ "key": "CONFIRMAR_COMPRA",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "confirmada": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "confirmada"
+ ],
+ "additionalProperties": false
+ }
+ }
+ ],
+ "transitions": [
+ {
+ "id": "asignar-cliente",
+ "from": "CREADA",
+ "event": "ASIGNAR_CLIENTE",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "cliente",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "CLIENTES_CONSULTA",
+ "contractVersion": "1",
+ "inputs": {
+ "client": {
+ "path": "/request/data/client"
+ }
+ },
+ "resultKey": "cliente"
+ }
+ ],
+ "decision": "CLIENTE_EXISTE",
+ "destinations": {
+ "CLIENTE_ENCONTRADO": "CONFIRMACION_PENDIENTE",
+ "CLIENTE_NO_ENCONTRADO": "CREADA"
+ },
+ "updates": [
+ {
+ "target": "/cliente",
+ "value": {
+ "path": "/results/cliente"
+ }
+ }
+ ]
+ },
+ {
+ "id": "confirmar-compra",
+ "from": "CONFIRMACION_PENDIENTE",
+ "event": "CONFIRMAR_COMPRA",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [],
+ "decision": "CONFIRMACION",
+ "destinations": {
+ "CONFIRMADA": "COMPRA_CONFIRMADA",
+ "CANCELADA": "COMPRA_CANCELADA"
+ },
+ "updates": [
+ {
+ "target": "/confirmada",
+ "value": {
+ "path": "/event/data/confirmada"
+ }
+ }
+ ]
+ }
+ ],
+ "decisions": [
+ {
+ "id": "CLIENTE_EXISTE",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "encontrado",
+ "when": {
+ "op": "EQ",
+ "left": {
+ "path": "/results/cliente/found"
+ },
+ "right": {
+ "literal": true
+ }
+ },
+ "output": "CLIENTE_ENCONTRADO"
+ }
+ ],
+ "defaultOutput": "CLIENTE_NO_ENCONTRADO"
+ },
+ {
+ "id": "CONFIRMACION",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "confirmada",
+ "when": {
+ "op": "EQ",
+ "left": {
+ "path": "/event/data/confirmada"
+ },
+ "right": {
+ "literal": true
+ }
+ },
+ "output": "CONFIRMADA"
+ }
+ ],
+ "defaultOutput": "CANCELADA"
+ }
+ ],
+ "tasks": [
+ {
+ "state": "CREADA",
+ "event": "ASIGNAR_CLIENTE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "FORM",
+ "title": "Consultar cliente",
+ "fieldOrder": []
+ }
+ },
+ {
+ "state": "CONFIRMACION_PENDIENTE",
+ "event": "CONFIRMAR_COMPRA",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "confirmada": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "confirmada"
+ ],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "FORM",
+ "title": "Confirmar o cancelar compra de demostración",
+ "fieldOrder": [
+ "confirmada"
+ ]
+ }
+ }
+ ]
+}
diff --git a/examples/flows/credito-v1.json b/examples/flows/credito-v1.json
new file mode 100644
index 0000000..fbae350
--- /dev/null
+++ b/examples/flows/credito-v1.json
@@ -0,0 +1,622 @@
+{
+ "schemaVersion": 1,
+ "flowKey": "CREDITO",
+ "version": 1,
+ "initialState": "CREADA",
+ "initialDataSchema": {
+ "type": "object",
+ "properties": {
+ "client": {
+ "type": "string",
+ "minLength": 1
+ },
+ "consentimientoId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "client",
+ "consentimientoId"
+ ],
+ "additionalProperties": false
+ },
+ "dataSchema": {
+ "type": "object",
+ "properties": {
+ "client": {
+ "type": "string",
+ "minLength": 1
+ },
+ "consentimientoId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "cliente": {
+ "type": "object",
+ "properties": {
+ "found": {
+ "type": "boolean"
+ },
+ "customerId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "found"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "found": {
+ "const": true
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "customerId"
+ ]
+ }
+ }
+ ]
+ },
+ "documentos": {
+ "type": "object",
+ "properties": {
+ "ine": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "cuenta": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [],
+ "additionalProperties": false
+ },
+ "validaciones": {
+ "type": "object",
+ "properties": {
+ "ine": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": [
+ "VALID",
+ "INVALID"
+ ]
+ },
+ "reasonCodes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "required": [
+ "status",
+ "reasonCodes"
+ ],
+ "additionalProperties": false
+ },
+ "cuenta": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": [
+ "VALID",
+ "INVALID"
+ ]
+ },
+ "reasonCodes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "required": [
+ "status",
+ "reasonCodes"
+ ],
+ "additionalProperties": false
+ },
+ "buro": {
+ "type": "object",
+ "properties": {
+ "score": {
+ "type": "number"
+ },
+ "consultationId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "score",
+ "consultationId"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "client",
+ "consentimientoId"
+ ],
+ "additionalProperties": false
+ },
+ "states": [
+ {
+ "key": "CREADA",
+ "terminal": false
+ },
+ {
+ "key": "INE_PENDIENTE",
+ "terminal": false
+ },
+ {
+ "key": "ESTADO_CUENTA_PENDIENTE",
+ "terminal": false
+ },
+ {
+ "key": "BURO_PENDIENTE",
+ "terminal": false
+ },
+ {
+ "key": "PASO_N",
+ "terminal": true
+ },
+ {
+ "key": "PASO_M",
+ "terminal": true
+ },
+ {
+ "key": "REVISION_MANUAL",
+ "terminal": true
+ }
+ ],
+ "events": [
+ {
+ "key": "ASIGNAR_CLIENTE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ }
+ },
+ {
+ "key": "REGISTRAR_INE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ }
+ },
+ {
+ "key": "REGISTRAR_ESTADO_CUENTA",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ }
+ },
+ {
+ "key": "VALIDAR_BURO",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ }
+ }
+ ],
+ "transitions": [
+ {
+ "id": "asignar-cliente",
+ "from": "CREADA",
+ "event": "ASIGNAR_CLIENTE",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "cliente",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "CLIENTES_CONSULTA",
+ "contractVersion": "1",
+ "inputs": {
+ "client": {
+ "path": "/request/data/client"
+ }
+ },
+ "resultKey": "cliente"
+ }
+ ],
+ "decision": "CLIENTE_EXISTE",
+ "destinations": {
+ "CLIENTE_ENCONTRADO": "INE_PENDIENTE",
+ "CLIENTE_NO_ENCONTRADO": "CREADA"
+ },
+ "updates": [
+ {
+ "target": "/cliente",
+ "value": {
+ "path": "/results/cliente"
+ }
+ }
+ ]
+ },
+ {
+ "id": "documento-ine",
+ "from": "INE_PENDIENTE",
+ "event": "REGISTRAR_INE",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "registro",
+ "type": "REGISTER_DOCUMENT",
+ "documentType": "INE",
+ "inputs": {
+ "documentId": {
+ "path": "/event/data/documentId"
+ }
+ },
+ "resultKey": "registro"
+ },
+ {
+ "id": "validacion",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "DOCUMENTO_VALIDA",
+ "contractVersion": "1",
+ "profile": "INE",
+ "profileVersion": "1",
+ "inputs": {
+ "documentId": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ "resultKey": "validacion"
+ }
+ ],
+ "decision": "VALIDAR_INE",
+ "destinations": {
+ "DOCUMENTO_VALIDO": "ESTADO_CUENTA_PENDIENTE",
+ "DOCUMENTO_INVALIDO": "INE_PENDIENTE"
+ },
+ "updates": [
+ {
+ "target": "/documentos/ine",
+ "value": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ {
+ "target": "/validaciones/ine",
+ "value": {
+ "path": "/results/validacion"
+ }
+ }
+ ]
+ },
+ {
+ "id": "documento-cuenta",
+ "from": "ESTADO_CUENTA_PENDIENTE",
+ "event": "REGISTRAR_ESTADO_CUENTA",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "registro",
+ "type": "REGISTER_DOCUMENT",
+ "documentType": "ESTADO_CUENTA",
+ "inputs": {
+ "documentId": {
+ "path": "/event/data/documentId"
+ }
+ },
+ "resultKey": "registro"
+ },
+ {
+ "id": "validacion",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "DOCUMENTO_VALIDA",
+ "contractVersion": "1",
+ "profile": "ESTADO_CUENTA",
+ "profileVersion": "1",
+ "inputs": {
+ "documentId": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ "resultKey": "validacion"
+ }
+ ],
+ "decision": "VALIDAR_ESTADO_CUENTA",
+ "destinations": {
+ "DOCUMENTO_VALIDO": "BURO_PENDIENTE",
+ "DOCUMENTO_INVALIDO": "ESTADO_CUENTA_PENDIENTE"
+ },
+ "updates": [
+ {
+ "target": "/documentos/cuenta",
+ "value": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ {
+ "target": "/validaciones/cuenta",
+ "value": {
+ "path": "/results/validacion"
+ }
+ }
+ ]
+ },
+ {
+ "id": "consultar-buro",
+ "from": "BURO_PENDIENTE",
+ "event": "VALIDAR_BURO",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "buro",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "BURO_CONSULTA",
+ "contractVersion": "1",
+ "inputs": {
+ "client": {
+ "path": "/request/data/cliente/customerId"
+ },
+ "consentimientoId": {
+ "path": "/request/data/consentimientoId"
+ }
+ },
+ "resultKey": "buro"
+ }
+ ],
+ "decision": "RUTA_SCORING",
+ "destinations": {
+ "N": "PASO_N",
+ "M": "PASO_M",
+ "MANUAL": "REVISION_MANUAL"
+ },
+ "updates": [
+ {
+ "target": "/validaciones/buro",
+ "value": {
+ "path": "/results/buro"
+ }
+ }
+ ]
+ }
+ ],
+ "decisions": [
+ {
+ "id": "CLIENTE_EXISTE",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "encontrado",
+ "when": {
+ "op": "EQ",
+ "left": {
+ "path": "/results/cliente/found"
+ },
+ "right": {
+ "literal": true
+ }
+ },
+ "output": "CLIENTE_ENCONTRADO"
+ }
+ ],
+ "defaultOutput": "CLIENTE_NO_ENCONTRADO"
+ },
+ {
+ "id": "VALIDAR_INE",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "valido",
+ "when": {
+ "op": "EQ",
+ "left": {
+ "path": "/results/validacion/status"
+ },
+ "right": {
+ "literal": "VALID"
+ }
+ },
+ "output": "DOCUMENTO_VALIDO"
+ }
+ ],
+ "defaultOutput": "DOCUMENTO_INVALIDO"
+ },
+ {
+ "id": "VALIDAR_ESTADO_CUENTA",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "valido",
+ "when": {
+ "op": "EQ",
+ "left": {
+ "path": "/results/validacion/status"
+ },
+ "right": {
+ "literal": "VALID"
+ }
+ },
+ "output": "DOCUMENTO_VALIDO"
+ }
+ ],
+ "defaultOutput": "DOCUMENTO_INVALIDO"
+ },
+ {
+ "id": "RUTA_SCORING",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "alto",
+ "when": {
+ "op": "GT",
+ "left": {
+ "path": "/results/buro/score"
+ },
+ "right": {
+ "literal": 70
+ }
+ },
+ "output": "N"
+ },
+ {
+ "id": "medio",
+ "when": {
+ "op": "AND",
+ "args": [
+ {
+ "op": "GT",
+ "left": {
+ "path": "/results/buro/score"
+ },
+ "right": {
+ "literal": 60
+ }
+ },
+ {
+ "op": "LTE",
+ "left": {
+ "path": "/results/buro/score"
+ },
+ "right": {
+ "literal": 70
+ }
+ }
+ ]
+ },
+ "output": "M"
+ }
+ ],
+ "defaultOutput": "MANUAL"
+ }
+ ],
+ "tasks": [
+ {
+ "state": "CREADA",
+ "event": "ASIGNAR_CLIENTE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "FORM",
+ "title": "Consultar cliente",
+ "fieldOrder": []
+ }
+ },
+ {
+ "state": "INE_PENDIENTE",
+ "event": "REGISTRAR_INE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "DOCUMENT_UPLOAD",
+ "title": "Identificación INE",
+ "documentType": "INE",
+ "acceptedMimeTypes": [
+ "application/pdf",
+ "image/jpeg",
+ "image/png"
+ ],
+ "maxBytes": 5242880
+ }
+ },
+ {
+ "state": "ESTADO_CUENTA_PENDIENTE",
+ "event": "REGISTRAR_ESTADO_CUENTA",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "DOCUMENT_UPLOAD",
+ "title": "Estado de cuenta",
+ "documentType": "ESTADO_CUENTA",
+ "acceptedMimeTypes": [
+ "application/pdf"
+ ],
+ "maxBytes": 5242880
+ }
+ },
+ {
+ "state": "BURO_PENDIENTE",
+ "event": "VALIDAR_BURO",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "FORM",
+ "title": "Consultar buró con consentimiento registrado",
+ "fieldOrder": []
+ }
+ }
+ ]
+}
diff --git a/examples/flows/credito-v2-licencia.json b/examples/flows/credito-v2-licencia.json
new file mode 100644
index 0000000..47195b6
--- /dev/null
+++ b/examples/flows/credito-v2-licencia.json
@@ -0,0 +1,772 @@
+{
+ "schemaVersion": 1,
+ "flowKey": "CREDITO",
+ "version": 2,
+ "initialState": "CREADA",
+ "initialDataSchema": {
+ "type": "object",
+ "properties": {
+ "client": {
+ "type": "string",
+ "minLength": 1
+ },
+ "consentimientoId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "client",
+ "consentimientoId"
+ ],
+ "additionalProperties": false
+ },
+ "dataSchema": {
+ "type": "object",
+ "properties": {
+ "client": {
+ "type": "string",
+ "minLength": 1
+ },
+ "consentimientoId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "cliente": {
+ "type": "object",
+ "properties": {
+ "found": {
+ "type": "boolean"
+ },
+ "customerId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "found"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "found": {
+ "const": true
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "customerId"
+ ]
+ }
+ }
+ ]
+ },
+ "documentos": {
+ "type": "object",
+ "properties": {
+ "ine": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "cuenta": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "licencia": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [],
+ "additionalProperties": false
+ },
+ "validaciones": {
+ "type": "object",
+ "properties": {
+ "ine": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": [
+ "VALID",
+ "INVALID"
+ ]
+ },
+ "reasonCodes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "required": [
+ "status",
+ "reasonCodes"
+ ],
+ "additionalProperties": false
+ },
+ "cuenta": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": [
+ "VALID",
+ "INVALID"
+ ]
+ },
+ "reasonCodes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "required": [
+ "status",
+ "reasonCodes"
+ ],
+ "additionalProperties": false
+ },
+ "licencia": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": [
+ "VALID",
+ "INVALID"
+ ]
+ },
+ "reasonCodes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "required": [
+ "status",
+ "reasonCodes"
+ ],
+ "additionalProperties": false
+ },
+ "buro": {
+ "type": "object",
+ "properties": {
+ "score": {
+ "type": "number"
+ },
+ "consultationId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "score",
+ "consultationId"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "client",
+ "consentimientoId"
+ ],
+ "additionalProperties": false
+ },
+ "states": [
+ {
+ "key": "CREADA",
+ "terminal": false
+ },
+ {
+ "key": "INE_PENDIENTE",
+ "terminal": false
+ },
+ {
+ "key": "ESTADO_CUENTA_PENDIENTE",
+ "terminal": false
+ },
+ {
+ "key": "LICENCIA_PENDIENTE",
+ "terminal": false
+ },
+ {
+ "key": "BURO_PENDIENTE",
+ "terminal": false
+ },
+ {
+ "key": "PASO_N",
+ "terminal": true
+ },
+ {
+ "key": "PASO_M",
+ "terminal": true
+ },
+ {
+ "key": "REVISION_MANUAL",
+ "terminal": true
+ }
+ ],
+ "events": [
+ {
+ "key": "ASIGNAR_CLIENTE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ }
+ },
+ {
+ "key": "REGISTRAR_INE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ }
+ },
+ {
+ "key": "REGISTRAR_ESTADO_CUENTA",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ }
+ },
+ {
+ "key": "REGISTRAR_LICENCIA",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ }
+ },
+ {
+ "key": "VALIDAR_BURO",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ }
+ }
+ ],
+ "transitions": [
+ {
+ "id": "asignar-cliente",
+ "from": "CREADA",
+ "event": "ASIGNAR_CLIENTE",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "cliente",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "CLIENTES_CONSULTA",
+ "contractVersion": "1",
+ "inputs": {
+ "client": {
+ "path": "/request/data/client"
+ }
+ },
+ "resultKey": "cliente"
+ }
+ ],
+ "decision": "CLIENTE_EXISTE",
+ "destinations": {
+ "CLIENTE_ENCONTRADO": "INE_PENDIENTE",
+ "CLIENTE_NO_ENCONTRADO": "CREADA"
+ },
+ "updates": [
+ {
+ "target": "/cliente",
+ "value": {
+ "path": "/results/cliente"
+ }
+ }
+ ]
+ },
+ {
+ "id": "documento-ine",
+ "from": "INE_PENDIENTE",
+ "event": "REGISTRAR_INE",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "registro",
+ "type": "REGISTER_DOCUMENT",
+ "documentType": "INE",
+ "inputs": {
+ "documentId": {
+ "path": "/event/data/documentId"
+ }
+ },
+ "resultKey": "registro"
+ },
+ {
+ "id": "validacion",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "DOCUMENTO_VALIDA",
+ "contractVersion": "1",
+ "profile": "INE",
+ "profileVersion": "1",
+ "inputs": {
+ "documentId": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ "resultKey": "validacion"
+ }
+ ],
+ "decision": "VALIDAR_INE",
+ "destinations": {
+ "DOCUMENTO_VALIDO": "ESTADO_CUENTA_PENDIENTE",
+ "DOCUMENTO_INVALIDO": "INE_PENDIENTE"
+ },
+ "updates": [
+ {
+ "target": "/documentos/ine",
+ "value": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ {
+ "target": "/validaciones/ine",
+ "value": {
+ "path": "/results/validacion"
+ }
+ }
+ ]
+ },
+ {
+ "id": "documento-cuenta",
+ "from": "ESTADO_CUENTA_PENDIENTE",
+ "event": "REGISTRAR_ESTADO_CUENTA",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "registro",
+ "type": "REGISTER_DOCUMENT",
+ "documentType": "ESTADO_CUENTA",
+ "inputs": {
+ "documentId": {
+ "path": "/event/data/documentId"
+ }
+ },
+ "resultKey": "registro"
+ },
+ {
+ "id": "validacion",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "DOCUMENTO_VALIDA",
+ "contractVersion": "1",
+ "profile": "ESTADO_CUENTA",
+ "profileVersion": "1",
+ "inputs": {
+ "documentId": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ "resultKey": "validacion"
+ }
+ ],
+ "decision": "VALIDAR_ESTADO_CUENTA",
+ "destinations": {
+ "DOCUMENTO_VALIDO": "LICENCIA_PENDIENTE",
+ "DOCUMENTO_INVALIDO": "ESTADO_CUENTA_PENDIENTE"
+ },
+ "updates": [
+ {
+ "target": "/documentos/cuenta",
+ "value": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ {
+ "target": "/validaciones/cuenta",
+ "value": {
+ "path": "/results/validacion"
+ }
+ }
+ ]
+ },
+ {
+ "id": "documento-licencia",
+ "from": "LICENCIA_PENDIENTE",
+ "event": "REGISTRAR_LICENCIA",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "registro",
+ "type": "REGISTER_DOCUMENT",
+ "documentType": "LICENCIA_CONDUCIR",
+ "inputs": {
+ "documentId": {
+ "path": "/event/data/documentId"
+ }
+ },
+ "resultKey": "registro"
+ },
+ {
+ "id": "validacion",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "DOCUMENTO_VALIDA",
+ "contractVersion": "1",
+ "profile": "LICENCIA_CONDUCIR",
+ "profileVersion": "1",
+ "inputs": {
+ "documentId": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ "resultKey": "validacion"
+ }
+ ],
+ "decision": "VALIDAR_LICENCIA_CONDUCIR",
+ "destinations": {
+ "DOCUMENTO_VALIDO": "BURO_PENDIENTE",
+ "DOCUMENTO_INVALIDO": "LICENCIA_PENDIENTE"
+ },
+ "updates": [
+ {
+ "target": "/documentos/licencia",
+ "value": {
+ "path": "/results/registro/documentId"
+ }
+ },
+ {
+ "target": "/validaciones/licencia",
+ "value": {
+ "path": "/results/validacion"
+ }
+ }
+ ]
+ },
+ {
+ "id": "consultar-buro",
+ "from": "BURO_PENDIENTE",
+ "event": "VALIDAR_BURO",
+ "allowedRoles": [
+ "ANALISTA"
+ ],
+ "actions": [
+ {
+ "id": "buro",
+ "type": "INVOKE_INTEGRATION",
+ "integrationKey": "BURO_CONSULTA",
+ "contractVersion": "1",
+ "inputs": {
+ "client": {
+ "path": "/request/data/cliente/customerId"
+ },
+ "consentimientoId": {
+ "path": "/request/data/consentimientoId"
+ }
+ },
+ "resultKey": "buro"
+ }
+ ],
+ "decision": "RUTA_SCORING",
+ "destinations": {
+ "N": "PASO_N",
+ "M": "PASO_M",
+ "MANUAL": "REVISION_MANUAL"
+ },
+ "updates": [
+ {
+ "target": "/validaciones/buro",
+ "value": {
+ "path": "/results/buro"
+ }
+ }
+ ]
+ }
+ ],
+ "decisions": [
+ {
+ "id": "CLIENTE_EXISTE",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "encontrado",
+ "when": {
+ "op": "EQ",
+ "left": {
+ "path": "/results/cliente/found"
+ },
+ "right": {
+ "literal": true
+ }
+ },
+ "output": "CLIENTE_ENCONTRADO"
+ }
+ ],
+ "defaultOutput": "CLIENTE_NO_ENCONTRADO"
+ },
+ {
+ "id": "VALIDAR_INE",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "valido",
+ "when": {
+ "op": "EQ",
+ "left": {
+ "path": "/results/validacion/status"
+ },
+ "right": {
+ "literal": "VALID"
+ }
+ },
+ "output": "DOCUMENTO_VALIDO"
+ }
+ ],
+ "defaultOutput": "DOCUMENTO_INVALIDO"
+ },
+ {
+ "id": "VALIDAR_ESTADO_CUENTA",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "valido",
+ "when": {
+ "op": "EQ",
+ "left": {
+ "path": "/results/validacion/status"
+ },
+ "right": {
+ "literal": "VALID"
+ }
+ },
+ "output": "DOCUMENTO_VALIDO"
+ }
+ ],
+ "defaultOutput": "DOCUMENTO_INVALIDO"
+ },
+ {
+ "id": "VALIDAR_LICENCIA_CONDUCIR",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "valido",
+ "when": {
+ "op": "EQ",
+ "left": {
+ "path": "/results/validacion/status"
+ },
+ "right": {
+ "literal": "VALID"
+ }
+ },
+ "output": "DOCUMENTO_VALIDO"
+ }
+ ],
+ "defaultOutput": "DOCUMENTO_INVALIDO"
+ },
+ {
+ "id": "RUTA_SCORING",
+ "hitPolicy": "UNIQUE",
+ "rules": [
+ {
+ "id": "alto",
+ "when": {
+ "op": "GT",
+ "left": {
+ "path": "/results/buro/score"
+ },
+ "right": {
+ "literal": 70
+ }
+ },
+ "output": "N"
+ },
+ {
+ "id": "medio",
+ "when": {
+ "op": "AND",
+ "args": [
+ {
+ "op": "GT",
+ "left": {
+ "path": "/results/buro/score"
+ },
+ "right": {
+ "literal": 60
+ }
+ },
+ {
+ "op": "LTE",
+ "left": {
+ "path": "/results/buro/score"
+ },
+ "right": {
+ "literal": 70
+ }
+ }
+ ]
+ },
+ "output": "M"
+ }
+ ],
+ "defaultOutput": "MANUAL"
+ }
+ ],
+ "tasks": [
+ {
+ "state": "CREADA",
+ "event": "ASIGNAR_CLIENTE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "FORM",
+ "title": "Consultar cliente",
+ "fieldOrder": []
+ }
+ },
+ {
+ "state": "INE_PENDIENTE",
+ "event": "REGISTRAR_INE",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "DOCUMENT_UPLOAD",
+ "title": "Identificación INE",
+ "documentType": "INE",
+ "acceptedMimeTypes": [
+ "application/pdf",
+ "image/jpeg",
+ "image/png"
+ ],
+ "maxBytes": 5242880
+ }
+ },
+ {
+ "state": "ESTADO_CUENTA_PENDIENTE",
+ "event": "REGISTRAR_ESTADO_CUENTA",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "DOCUMENT_UPLOAD",
+ "title": "Estado de cuenta",
+ "documentType": "ESTADO_CUENTA",
+ "acceptedMimeTypes": [
+ "application/pdf"
+ ],
+ "maxBytes": 5242880
+ }
+ },
+ {
+ "state": "LICENCIA_PENDIENTE",
+ "event": "REGISTRAR_LICENCIA",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "DOCUMENT_UPLOAD",
+ "title": "Licencia de conducir",
+ "documentType": "LICENCIA_CONDUCIR",
+ "acceptedMimeTypes": [
+ "application/pdf",
+ "image/jpeg",
+ "image/png"
+ ],
+ "maxBytes": 5242880
+ }
+ },
+ {
+ "state": "BURO_PENDIENTE",
+ "event": "VALIDAR_BURO",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ "additionalProperties": false
+ },
+ "ui": {
+ "component": "FORM",
+ "title": "Consultar buró con consentimiento registrado",
+ "fieldOrder": []
+ }
+ }
+ ]
+}
diff --git a/examples/flows/seed-request-types.sql b/examples/flows/seed-request-types.sql
new file mode 100644
index 0000000..03287a6
--- /dev/null
+++ b/examples/flows/seed-request-types.sql
@@ -0,0 +1,4 @@
+-- Solo entorno de demo, como dueño del esquema; ajustar orchestrator al esquema configurado.
+INSERT INTO orchestrator.request_type(type_key)
+VALUES ('CREDITO'), ('COMPRA')
+ON CONFLICT (type_key) DO NOTHING;
diff --git a/examples/http/available-events.http b/examples/http/available-events.http
new file mode 100644
index 0000000..e2a1efb
--- /dev/null
+++ b/examples/http/available-events.http
@@ -0,0 +1,27 @@
+@baseUrl = http://localhost:8787
+@requestId = 00000000-0000-0000-0000-000000000001
+@jwt = REPLACE_WITH_OWNER_JWT
+@revision = 0
+@event = SUBMIT
+@documentId = 00000000-0000-0000-0000-000000000002
+
+### Requiere REQUEST_API_ENABLED=true. Usa la versión fijada a esta solicitud.
+GET {{baseUrl}}/api/v1/requests/{{requestId}}/available-events
+Authorization: Bearer {{jwt}}
+
+### Solo si no hay blockedByExecutionId. Sustituir evento/datos según inputSchema.
+### documentId debe existir y pertenecer a esta solicitud en el servicio documental.
+POST {{baseUrl}}/api/v1/requests/{{requestId}}/events
+Authorization: Bearer {{jwt}}
+Idempotency-Key: document-submission-demo-1
+If-Match: "{{revision}}"
+Content-Type: application/json
+
+{
+ "event": "{{event}}",
+ "data": {"documentId": "{{documentId}}"}
+}
+
+### Tras completar la ejecución, consultar datos/razones de negocio y las tareas otra vez.
+GET {{baseUrl}}/api/v1/requests/{{requestId}}
+Authorization: Bearer {{jwt}}
diff --git a/examples/http/demo-flows.http b/examples/http/demo-flows.http
new file mode 100644
index 0000000..4839df7
--- /dev/null
+++ b/examples/http/demo-flows.http
@@ -0,0 +1,176 @@
+# Entorno de demo exclusivamente. Sustituir tokens e IDs con respuestas reales.
+@base = http://localhost:8080
+@editor = REEMPLAZAR
+@publisher = REEMPLAZAR
+@manager = REEMPLAZAR
+@analista = REEMPLAZAR
+@creditoV1 = REEMPLAZAR_UUID
+@creditoV2 = REEMPLAZAR_UUID
+@compraV1 = REEMPLAZAR_UUID
+@requestId = REEMPLAZAR_UUID
+@executionId = REEMPLAZAR_UUID
+@documentId = REEMPLAZAR_UUID
+@revision = 0
+@typeRevision = 0
+@eventKey = REEMPLAZAR_CLAVE_UNICA
+
+### Crear credito-v1; copiar id a creditoV1
+POST {{base}}/api/v1/admin/flows
+Authorization: Bearer {{editor}}
+Content-Type: application/json
+
+< ../flows/credito-v1.json
+
+### Validar credito-v1
+POST {{base}}/api/v1/admin/flows/{{creditoV1}}/validate
+Authorization: Bearer {{editor}}
+
+### Publicar credito-v1; revisión inicial 0, publicado 1
+POST {{base}}/api/v1/admin/flows/{{creditoV1}}/publish
+Authorization: Bearer {{publisher}}
+If-Match: "0"
+
+### Crear credito-v2-licencia; copiar id a creditoV2
+POST {{base}}/api/v1/admin/flows
+Authorization: Bearer {{editor}}
+Content-Type: application/json
+
+< ../flows/credito-v2-licencia.json
+
+### Validar credito-v2-licencia
+POST {{base}}/api/v1/admin/flows/{{creditoV2}}/validate
+Authorization: Bearer {{editor}}
+
+### Publicar credito-v2-licencia; revisión inicial 0, publicado 1
+POST {{base}}/api/v1/admin/flows/{{creditoV2}}/publish
+Authorization: Bearer {{publisher}}
+If-Match: "0"
+
+### Crear compra-v1; copiar id a compraV1
+POST {{base}}/api/v1/admin/flows
+Authorization: Bearer {{editor}}
+Content-Type: application/json
+
+< ../flows/compra-v1.json
+
+### Validar compra-v1
+POST {{base}}/api/v1/admin/flows/{{compraV1}}/validate
+Authorization: Bearer {{editor}}
+
+### Publicar compra-v1; revisión inicial 0, publicado 1
+POST {{base}}/api/v1/admin/flows/{{compraV1}}/publish
+Authorization: Bearer {{publisher}}
+If-Match: "0"
+
+### Activar creditoV1. Para v2: crear primero una solicitud v1; usar revisión actual del tipo.
+PUT {{base}}/api/v1/admin/request-types/CREDITO/active-flow
+Authorization: Bearer {{manager}}
+Content-Type: application/json
+If-Match: "{{typeRevision}}"
+
+{"flowId":"{{creditoV1}}"}
+
+### Activar compraV1. Para v2: crear primero una solicitud v1; usar revisión actual del tipo.
+PUT {{base}}/api/v1/admin/request-types/COMPRA/active-flow
+Authorization: Bearer {{manager}}
+Content-Type: application/json
+If-Match: "{{typeRevision}}"
+
+{"flowId":"{{compraV1}}"}
+
+### Activar creditoV2. Para v2: crear primero una solicitud v1; usar revisión actual del tipo.
+PUT {{base}}/api/v1/admin/request-types/CREDITO/active-flow
+Authorization: Bearer {{manager}}
+Content-Type: application/json
+If-Match: "{{typeRevision}}"
+
+{"flowId":"{{creditoV2}}"}
+
+### Crear crédito (ejecutar antes y después de activar v2, con claves distintas)
+POST {{base}}/api/v1/requests
+Authorization: Bearer {{analista}}
+Content-Type: application/json
+Idempotency-Key: {{eventKey}}
+
+{"typeKey":"CREDITO","data":{"client":"synthetic-client","consentimientoId":"synthetic-consent"}}
+
+### Crear compra
+POST {{base}}/api/v1/requests
+Authorization: Bearer {{analista}}
+Content-Type: application/json
+Idempotency-Key: {{eventKey}}
+
+{"typeKey":"COMPRA","data":{"client":"synthetic-client","orderId":"synthetic-order","amount":100}}
+
+### Consultar solicitud: copiar revisión a revision, comprobar flow.id y currentState
+GET {{base}}/api/v1/requests/{{requestId}}
+Authorization: Bearer {{analista}}
+
+### Consultar tarea disponible y restricciones documentales
+GET {{base}}/api/v1/requests/{{requestId}}/available-events
+Authorization: Bearer {{analista}}
+
+### Consultar ejecución hasta SUCCEEDED; FAILED/RETRY_WAIT requieren atender causa
+GET {{base}}/api/v1/executions/{{executionId}}
+Authorization: Bearer {{analista}}
+
+### ASIGNAR_CLIENTE: revisar tarea, actualizar revision y eventKey; licencia solo v2, confirmar solo compra
+POST {{base}}/api/v1/requests/{{requestId}}/events
+Authorization: Bearer {{analista}}
+Content-Type: application/json
+If-Match: "{{revision}}"
+Idempotency-Key: {{eventKey}}
+
+{"event":"ASIGNAR_CLIENTE","data":{}}
+
+### REGISTRAR_INE: revisar tarea, actualizar revision y eventKey; licencia solo v2, confirmar solo compra
+POST {{base}}/api/v1/requests/{{requestId}}/events
+Authorization: Bearer {{analista}}
+Content-Type: application/json
+If-Match: "{{revision}}"
+Idempotency-Key: {{eventKey}}
+
+{"event":"REGISTRAR_INE","data":{"documentId":"{{documentId}}"}}
+
+### REGISTRAR_ESTADO_CUENTA: revisar tarea, actualizar revision y eventKey; licencia solo v2, confirmar solo compra
+POST {{base}}/api/v1/requests/{{requestId}}/events
+Authorization: Bearer {{analista}}
+Content-Type: application/json
+If-Match: "{{revision}}"
+Idempotency-Key: {{eventKey}}
+
+{"event":"REGISTRAR_ESTADO_CUENTA","data":{"documentId":"{{documentId}}"}}
+
+### REGISTRAR_LICENCIA: revisar tarea, actualizar revision y eventKey; licencia solo v2, confirmar solo compra
+POST {{base}}/api/v1/requests/{{requestId}}/events
+Authorization: Bearer {{analista}}
+Content-Type: application/json
+If-Match: "{{revision}}"
+Idempotency-Key: {{eventKey}}
+
+{"event":"REGISTRAR_LICENCIA","data":{"documentId":"{{documentId}}"}}
+
+### VALIDAR_BURO: revisar tarea, actualizar revision y eventKey; licencia solo v2, confirmar solo compra
+POST {{base}}/api/v1/requests/{{requestId}}/events
+Authorization: Bearer {{analista}}
+Content-Type: application/json
+If-Match: "{{revision}}"
+Idempotency-Key: {{eventKey}}
+
+{"event":"VALIDAR_BURO","data":{}}
+
+### CONFIRMAR_COMPRA: revisar tarea, actualizar revision y eventKey; licencia solo v2, confirmar solo compra
+POST {{base}}/api/v1/requests/{{requestId}}/events
+Authorization: Bearer {{analista}}
+Content-Type: application/json
+If-Match: "{{revision}}"
+Idempotency-Key: {{eventKey}}
+
+{"event":"CONFIRMAR_COMPRA","data":{"confirmada":true}}
+
+# Probar confirmada:false en otra compra para COMPRA_CANCELADA.
+# Configurar proveedor mock con score 71/70/61/60 para N/M/M/REVISION_MANUAL.
+# INVALID + reasonCodes:["EXPIRED"] conserva LICENCIA_PENDIENTE; nuevo evento VALID avanza.
+# Cada ensayo de score usa otra solicitud y documentos propios. No enviar score desde el cliente.
+# DemoFlowsIT automatiza estas variantes sin editar rutas ni contactar proveedores reales.
+
diff --git a/examples/http/events.http b/examples/http/events.http
new file mode 100644
index 0000000..982864d
--- /dev/null
+++ b/examples/http/events.http
@@ -0,0 +1,18 @@
+### Obtener revision desde GET /requests/{id}; conservarla al reintentar este comando.
+### Variables: baseUrl, token, requestId, eventKey, requestRevision.
+POST {{baseUrl}}/api/v1/requests/{{requestId}}/events
+Authorization: Bearer {{token}}
+Idempotency-Key: demo-event-001
+If-Match: "{{requestRevision}}"
+Content-Type: application/json
+
+{"event":"{{eventKey}}","data":{}}
+
+### Ajustar data al inputSchema del evento. 202 confirma aceptación, no ejecución.
+### T10 procesa cuando WORKER_ENABLED=true; configurar adapters T11 antes de flujos externos.
+GET {{baseUrl}}/api/v1/requests/{{requestId}}
+Authorization: Bearer {{token}}
+
+### T10: consultar ejecucion aceptada (requiere REQUEST_API_ENABLED=true).
+GET {{baseUrl}}/api/v1/executions/{{executionId}}
+Authorization: Bearer {{token}}
diff --git a/examples/http/history-operations.http b/examples/http/history-operations.http
new file mode 100644
index 0000000..46acea4
--- /dev/null
+++ b/examples/http/history-operations.http
@@ -0,0 +1,48 @@
+@base = http://localhost:8080
+@analista = REEMPLAZAR_TOKEN_PROPIETARIO
+@operator = REEMPLAZAR_TOKEN_EXECUTION_OPERATOR
+@requestId = REEMPLAZAR_UUID
+@executionId = REEMPLAZAR_UUID
+@cursor = REEMPLAZAR_NEXT_CURSOR
+
+### Página inicial: copiar nextCursor sin modificarlo
+GET {{base}}/api/v1/requests/{{requestId}}/history?limit=50
+Authorization: Bearer {{analista}}
+
+### Página siguiente, con los mismos filtros
+GET {{base}}/api/v1/requests/{{requestId}}/history?limit=50&cursor={{cursor}}
+Authorization: Bearer {{analista}}
+
+### Filtros; from inclusivo, to exclusivo. Cursor nuevo para estos filtros.
+GET {{base}}/api/v1/requests/{{requestId}}/history?kind=TRANSITION_COMPLETED&executionId={{executionId}}&from=2026-01-01T00:00:00Z&to=2027-01-01T00:00:00Z
+Authorization: Bearer {{analista}}
+
+### Salud pública, sin detalles
+GET {{base}}/actuator/health/liveness
+
+### Readiness incluye PostgreSQL
+GET {{base}}/actuator/health/readiness
+
+### Requiere rol operador y ámbito incluido en ADMIN_BUSINESS_SCOPES
+GET {{base}}/actuator/metrics
+Authorization: Bearer {{operator}}
+
+### Estados técnicos (no sumar réplicas que consultan la misma DB)
+GET {{base}}/actuator/metrics/orchestrator.executions?tag=state:PENDING
+Authorization: Bearer {{operator}}
+
+### Antigüedad del pendiente más viejo, segundos
+GET {{base}}/actuator/metrics/orchestrator.pending.oldest.seconds
+Authorization: Bearer {{operator}}
+
+### Estado de la muestra SQL; 0 indica que los gauges de cola son desconocidos
+GET {{base}}/actuator/metrics/orchestrator.queue.sample.available
+Authorization: Bearer {{operator}}
+
+### Comandos HTTP (incluye replays); contador local al proceso
+GET {{base}}/actuator/metrics/orchestrator.commands?tag=command:event&tag=result:accepted
+Authorization: Bearer {{operator}}
+
+### Timer disponible tras invocar una integración; solo claves configuradas
+GET {{base}}/actuator/metrics/orchestrator.integration.duration?tag=integration:BURO_CONSULTA
+Authorization: Bearer {{operator}}
diff --git a/examples/http/reconciliation.http b/examples/http/reconciliation.http
new file mode 100644
index 0000000..e257980
--- /dev/null
+++ b/examples/http/reconciliation.http
@@ -0,0 +1,39 @@
+@baseUrl = http://localhost:8787
+@executionId = 00000000-0000-0000-0000-000000000001
+@operatorJwt = REPLACE_WITH_EXECUTION_OPERATOR_JWT
+
+### Solo después de verificar el efecto y el resultado con el proveedor.
+POST {{baseUrl}}/api/v1/admin/executions/{{executionId}}/resolve
+Authorization: Bearer {{operatorJwt}}
+Idempotency-Key: case-demo-resume-1
+Content-Type: application/json
+
+{
+ "mode": "RESUME",
+ "evidence": {
+ "reference": "CASE-DEMO-001",
+ "reason": "Ejemplo sintético: sustituir por evidencia verificada del caso real."
+ },
+ "verifiedActions": [
+ {
+ "actionId": "lookup",
+ "result": {"score": 70},
+ "providerReference": "PROVIDER-VERIFIED-REFERENCE"
+ }
+ ]
+}
+
+### Alternativa excluyente: cerrar únicamente tras verificar ausencia/compensación.
+POST {{baseUrl}}/api/v1/admin/executions/{{executionId}}/resolve
+Authorization: Bearer {{operatorJwt}}
+Idempotency-Key: case-demo-close-1
+Content-Type: application/json
+
+{
+ "mode": "CLOSE_WITHOUT_TRANSITION",
+ "evidence": {
+ "reference": "CASE-DEMO-002",
+ "reason": "Ejemplo sintético: sustituir por evidencia de compensación verificada."
+ },
+ "effectStatus": "COMPENSATED_VERIFIED"
+}
diff --git a/examples/http/requests.http b/examples/http/requests.http
new file mode 100644
index 0000000..173a864
--- /dev/null
+++ b/examples/http/requests.http
@@ -0,0 +1,13 @@
+### Requiere tipo ya activado, REQUEST_API_ENABLED=true y JWT válido con business_scope.
+### Variables del cliente HTTP: baseUrl, token, typeKey, requestId.
+POST {{baseUrl}}/api/v1/requests
+Authorization: Bearer {{token}}
+Idempotency-Key: demo-create-001
+Content-Type: application/json
+
+{"typeKey":"{{typeKey}}","data":{}}
+
+### Repetir POST con la misma clave/cuerpo retorna la respuesta original.
+### Ajustar data al initialDataSchema/dataSchema del flujo publicado.
+GET {{baseUrl}}/api/v1/requests/{{requestId}}
+Authorization: Bearer {{token}}
diff --git a/examples/http/simulate.http b/examples/http/simulate.http
new file mode 100644
index 0000000..20ea491
--- /dev/null
+++ b/examples/http/simulate.http
@@ -0,0 +1,24 @@
+@base = http://localhost:8080
+@editor = REEMPLAZAR_TOKEN_FLOW_EDITOR
+@flowId = REEMPLAZAR_UUID_CREDITO_V1
+
+### Simulación pura de una transición, usando fixtures sintéticos
+POST {{base}}/api/v1/admin/flows/{{flowId}}/simulate
+Authorization: Bearer {{editor}}
+Content-Type: application/json
+
+{
+ "state": "BURO_PENDIENTE",
+ "requestData": {
+ "client": "synthetic-client",
+ "consentimientoId": "synthetic-consent",
+ "cliente": {"found": true, "customerId": "synthetic-customer"}
+ },
+ "event": {"event": "VALIDAR_BURO", "data": {}},
+ "actor": {"subject": "synthetic-analyst", "businessScope": "demo", "roles": ["ANALISTA"]},
+ "results": {"buro": {"score": 71, "consultationId": "synthetic-consultation"}}
+}
+
+# Cambiar score a 70, 61 y 60: PASO_M, PASO_M y REVISION_MANUAL.
+# score string o ausente es un error de contrato, sin ruta por defecto.
+# El actor de fixture no sustituye el JWT del editor. No invoca buró real.
diff --git a/examples/integrations/integration-routes.example.json b/examples/integrations/integration-routes.example.json
new file mode 100644
index 0000000..1d555d7
--- /dev/null
+++ b/examples/integrations/integration-routes.example.json
@@ -0,0 +1,82 @@
+{
+ "routes": [
+ {
+ "integrationKey": "CLIENTES_CONSULTA",
+ "contractVersion": "1",
+ "profile": "",
+ "profileVersion": "",
+ "url": "https://provider.example.invalid/clientes_consulta",
+ "tokenEnvironment": "PROVIDER_TOKEN",
+ "connectMs": 1000,
+ "timeoutMs": 5000,
+ "maxBytes": 262144,
+ "outputMappings": {},
+ "notFoundOutput": null
+ },
+ {
+ "integrationKey": "DOCUMENTO_VALIDA",
+ "contractVersion": "1",
+ "profile": "LICENCIA_CONDUCIR",
+ "profileVersion": "1",
+ "url": "https://provider.example.invalid/documento_valida",
+ "tokenEnvironment": "PROVIDER_TOKEN",
+ "connectMs": 1000,
+ "timeoutMs": 5000,
+ "maxBytes": 262144,
+ "outputMappings": {},
+ "notFoundOutput": null
+ },
+ {
+ "integrationKey": "BURO_CONSULTA",
+ "contractVersion": "1",
+ "profile": "",
+ "profileVersion": "",
+ "url": "https://provider.example.invalid/buro_consulta",
+ "tokenEnvironment": "PROVIDER_TOKEN",
+ "connectMs": 1000,
+ "timeoutMs": 5000,
+ "maxBytes": 262144,
+ "outputMappings": {},
+ "notFoundOutput": null
+ },
+ {
+ "integrationKey": "DOCUMENTS_METADATA",
+ "contractVersion": "1",
+ "profile": "",
+ "profileVersion": "",
+ "url": "https://provider.example.invalid/documents_metadata",
+ "tokenEnvironment": "PROVIDER_TOKEN",
+ "connectMs": 1000,
+ "timeoutMs": 5000,
+ "maxBytes": 262144,
+ "outputMappings": {},
+ "notFoundOutput": null
+ },
+ {
+ "integrationKey": "DOCUMENTO_VALIDA",
+ "contractVersion": "1",
+ "profile": "INE",
+ "profileVersion": "1",
+ "url": "https://provider.example.invalid/documento_valida",
+ "tokenEnvironment": "PROVIDER_TOKEN",
+ "connectMs": 1000,
+ "timeoutMs": 5000,
+ "maxBytes": 262144,
+ "outputMappings": {},
+ "notFoundOutput": null
+ },
+ {
+ "integrationKey": "DOCUMENTO_VALIDA",
+ "contractVersion": "1",
+ "profile": "ESTADO_CUENTA",
+ "profileVersion": "1",
+ "url": "https://provider.example.invalid/documento_valida",
+ "tokenEnvironment": "PROVIDER_TOKEN",
+ "connectMs": 1000,
+ "timeoutMs": 5000,
+ "maxBytes": 262144,
+ "outputMappings": {},
+ "notFoundOutput": null
+ }
+ ]
+}
diff --git a/examples/integrations/publication-catalog.json b/examples/integrations/publication-catalog.json
new file mode 100644
index 0000000..2f4c590
--- /dev/null
+++ b/examples/integrations/publication-catalog.json
@@ -0,0 +1,361 @@
+{
+ "roles": [
+ "ANALISTA"
+ ],
+ "handlers": {
+ "INVOKE_INTEGRATION": {
+ "effect": "READ_ONLY",
+ "configurationSchema": {},
+ "inputSchema": {},
+ "outputSchema": {}
+ },
+ "REGISTER_DOCUMENT": {
+ "effect": "READ_ONLY",
+ "configurationSchema": {},
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "requestId": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "businessScope": {
+ "type": "string",
+ "minLength": 1
+ },
+ "documentType": {
+ "type": "string",
+ "minLength": 1
+ },
+ "storageDocumentId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "mimeType": {
+ "type": "string",
+ "minLength": 1
+ },
+ "sizeBytes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "checksum": {
+ "type": "string",
+ "pattern": "^[a-fA-F0-9]{64}$"
+ }
+ },
+ "required": [
+ "documentId",
+ "requestId",
+ "businessScope",
+ "documentType",
+ "storageDocumentId",
+ "mimeType",
+ "sizeBytes",
+ "checksum"
+ ],
+ "additionalProperties": false
+ }
+ }
+ },
+ "integrations": {
+ "CLIENTES_CONSULTA": {
+ "defaultVersion": "1",
+ "contracts": {
+ "1": {
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "client": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "client"
+ ],
+ "additionalProperties": false
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "found": {
+ "type": "boolean"
+ },
+ "customerId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "found"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "found": {
+ "const": true
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "customerId"
+ ]
+ }
+ }
+ ]
+ },
+ "effect": "READ_ONLY",
+ "profiles": {},
+ "defaultProfiles": {}
+ }
+ }
+ },
+ "DOCUMENTO_VALIDA": {
+ "defaultVersion": "1",
+ "contracts": {
+ "1": {
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "documentId"
+ ],
+ "additionalProperties": false
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": [
+ "VALID",
+ "INVALID"
+ ]
+ },
+ "reasonCodes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "required": [
+ "status",
+ "reasonCodes"
+ ],
+ "additionalProperties": false
+ },
+ "effect": "READ_ONLY",
+ "profiles": {
+ "LICENCIA_CONDUCIR": [
+ "1"
+ ],
+ "INE": [
+ "1"
+ ],
+ "ESTADO_CUENTA": [
+ "1"
+ ]
+ },
+ "defaultProfiles": {
+ "LICENCIA_CONDUCIR": "1",
+ "INE": "1",
+ "ESTADO_CUENTA": "1"
+ }
+ }
+ }
+ },
+ "BURO_CONSULTA": {
+ "defaultVersion": "1",
+ "contracts": {
+ "1": {
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "client": {
+ "type": "string",
+ "minLength": 1
+ },
+ "consentimientoId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "client",
+ "consentimientoId"
+ ],
+ "additionalProperties": false
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "score": {
+ "type": "number"
+ },
+ "consultationId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "score",
+ "consultationId"
+ ],
+ "additionalProperties": false
+ },
+ "effect": "READ_ONLY",
+ "profiles": {},
+ "defaultProfiles": {}
+ }
+ }
+ },
+ "DOCUMENTS_METADATA": {
+ "defaultVersion": "1",
+ "contracts": {
+ "1": {
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "requestId": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "businessScope": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "documentId",
+ "requestId",
+ "businessScope"
+ ],
+ "additionalProperties": false
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "documentId": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "requestId": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "businessScope": {
+ "type": "string",
+ "minLength": 1
+ },
+ "documentType": {
+ "type": "string",
+ "minLength": 1
+ },
+ "storageDocumentId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "mimeType": {
+ "type": "string",
+ "minLength": 1
+ },
+ "sizeBytes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "checksum": {
+ "type": "string",
+ "pattern": "^[a-fA-F0-9]{64}$"
+ },
+ "scanStatus": {
+ "type": "string",
+ "enum": [
+ "CLEAN",
+ "PENDING",
+ "INFECTED"
+ ]
+ }
+ },
+ "required": [
+ "documentId",
+ "requestId",
+ "businessScope",
+ "documentType",
+ "storageDocumentId",
+ "mimeType",
+ "sizeBytes",
+ "checksum",
+ "scanStatus"
+ ],
+ "additionalProperties": false
+ },
+ "effect": "READ_ONLY",
+ "profiles": {},
+ "defaultProfiles": {}
+ }
+ }
+ }
+ },
+ "documents": {
+ "DOCUMENT": {
+ "mimeTypes": [
+ "application/pdf"
+ ],
+ "maxBytes": 1000
+ },
+ "LICENCIA_CONDUCIR": {
+ "mimeTypes": [
+ "application/pdf",
+ "image/jpeg",
+ "image/png"
+ ],
+ "maxBytes": 5242880
+ },
+ "INE": {
+ "mimeTypes": [
+ "application/pdf",
+ "image/jpeg",
+ "image/png"
+ ],
+ "maxBytes": 5242880
+ },
+ "ESTADO_CUENTA": {
+ "mimeTypes": [
+ "application/pdf"
+ ],
+ "maxBytes": 5242880
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 7f9da03..63f2eac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,10 +30,15 @@
org.flywaydbflyway-database-postgresqlorg.postgresqlpostgresqlruntimeorg.junit.jupiterjunit-jupitertest
- com.networkntjson-schema-validator1.5.9test
+ com.networkntjson-schema-validator1.5.9io.swagger.parser.v3swagger-parser-v32.1.35test
+ org.testcontainerstestcontainers-postgresqltest
+
+ src/main/resources
+ schemasschemas*.schema.json
+ org.springframework.bootspring-boot-maven-plugin
@@ -47,14 +52,25 @@
org.apache.maven.pluginsmaven-surefire-plugin3.5.4
+ trueorg.apache.maven.pluginsmaven-failsafe-plugin3.5.4
+ trueintegration-testverify
+
+ migration-tests-external
+
+ org.apache.maven.pluginsmaven-failsafe-plugin
+
+ true
+
+
+ postgres96-check
diff --git a/schemas/README.md b/schemas/README.md
index 005e6de..e60e485 100644
--- a/schemas/README.md
+++ b/schemas/README.md
@@ -25,7 +25,7 @@ Condiciones:
- AND/OR: op y args con al menos dos condiciones ordenadas.
- NOT: op y arg con una condición.
-Tipos, BigDecimal, cortocircuito, null frente a ausencia, IN homogéneo, solapamientos y UNIQUE ambiguo se evaluarán en T05. UNIQUE requiere defaultOutput; FIRST sólo se habilita seleccionándolo explícitamente como hitPolicy.
+T05 implementa tipos, BigDecimal, cortocircuito, null frente a ausencia, IN homogéneo, análisis de intervalos simples y UNIQUE ambiguo. Ver [evaluador y límites](../specs/DECISION_EVALUATOR.md); el parser/publicador completo corresponde a T06. UNIQUE requiere defaultOutput; FIRST sólo se habilita seleccionándolo explícitamente como hitPolicy.
Transiciones: id/from/event/allowedRoles/actions. Una ruta fija usa to + outcome; una ruta calculada usa decision + destinations y el output seleccionado es el outcome de negocio. No pueden mezclarse. Actions están ordenadas y pueden estar vacías para una captura pura. Guard es opcional y se evalúa antes de aceptar el evento. Updates opcionales se aplican tras las acciones/decisión.
diff --git a/scripts/test_verify_acceptance.py b/scripts/test_verify_acceptance.py
new file mode 100644
index 0000000..99d5111
--- /dev/null
+++ b/scripts/test_verify_acceptance.py
@@ -0,0 +1,35 @@
+import tempfile
+from pathlib import Path
+import unittest
+from verify_acceptance import inspect
+
+
+class ReportGateTest(unittest.TestCase):
+ def test_missing_reports_cannot_pass(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ result = inspect(Path(tmp), {f"A{i:02}": ["Suite#case"] for i in range(1, 21)})
+ self.assertFalse(result["passed"])
+
+ def test_passed_skipped_failed_and_external_reports(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ for folder in ("surefire-reports", "failsafe-reports"):
+ (root / folder).mkdir()
+ (root / folder / "TEST-Suite.xml").write_text('')
+ manifest = {f"A{i:02}": ["Suite#case"] for i in range(1, 21)}
+ self.assertTrue(inspect(root, manifest)["passed"])
+ extra = root / "surefire-reports/TEST-Extra.xml"
+ extra.write_text('')
+ self.assertFalse(inspect(root, manifest)["passed"], "Even an unmapped skipped test must fail the gate")
+ extra.unlink()
+ for status in ("failure", "error", "skipped"):
+ (root / "failsafe-reports/TEST-Suite.xml").write_text(f'<{status}/>')
+ self.assertFalse(inspect(root, manifest)["passed"])
+ (root / "failsafe-reports/TEST-Suite.xml").write_text('')
+ self.assertEqual(["external-postgresql"], inspect(root, manifest)["environment"])
+ manifest["A03"] = ["Suite#missing"]
+ self.assertFalse(inspect(root, manifest)["passed"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/verify_acceptance.py b/scripts/verify_acceptance.py
new file mode 100644
index 0000000..fd1ae6a
--- /dev/null
+++ b/scripts/verify_acceptance.py
@@ -0,0 +1,77 @@
+"""Check actual Maven reports against A01-A20; no third-party packages or credentials."""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import re
+import sys
+import xml.etree.ElementTree as ET
+
+
+def inspect(root, manifest):
+ failures, found, totals, environments = [], {}, {"unit": 0, "integration": 0}, set()
+ expected = {f"A{i:02}" for i in range(1, 21)}
+ if set(manifest) != expected:
+ failures.append("Manifest must contain exactly A01-A20")
+ for group, folder in (("unit", "surefire-reports"), ("integration", "failsafe-reports")):
+ paths = sorted((root / folder).glob("TEST-*.xml"))
+ if not paths:
+ failures.append(f"Missing {folder}")
+ for path in paths:
+ suite = ET.parse(path).getroot()
+ for flag in ("failures", "errors", "skipped"):
+ if int(suite.get(flag, "0")):
+ failures.append(f"{suite.get('name')}: {flag}={suite.get(flag)}")
+ tests = suite.findall("testcase")
+ totals[group] += len(tests)
+ if group == "integration":
+ external = any(p.get("name") == "migration.test.external" and p.get("value") == "true"
+ for p in suite.findall("./properties/property"))
+ environments.add("external-postgresql" if external else "testcontainers")
+ for test in tests:
+ method = re.split(r"[\[(]", test.get("name", ""), maxsplit=1)[0]
+ key = test.get("classname", "").rsplit(".", 1)[-1] + "#" + method
+ ok = not any(test.find(tag) is not None for tag in ("failure", "error", "skipped"))
+ if not ok:
+ failures.append(f"Unsuccessful testcase: {key}")
+ found.setdefault(key, []).append(ok)
+ cases = {}
+ for case, references in sorted(manifest.items()):
+ if not references:
+ failures.append(f"{case}: no test references")
+ evidence = {}
+ for reference in references:
+ runs = found.get(reference, [])
+ evidence[reference] = {"executed": len(runs), "passed": sum(runs)}
+ if not runs or not all(runs):
+ failures.append(f"{case}: missing/failed/skipped {reference}")
+ cases[case] = evidence
+ if len(environments) != 1:
+ failures.append("Missing or mixed integration environments")
+ return {"passed": not failures, "environment": sorted(environments), "tests": totals,
+ "cases": cases, "failures": failures}
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--reports", type=Path, default=Path("target"))
+ parser.add_argument("--manifest", type=Path, default=Path("specs/acceptance-cases.json"))
+ parser.add_argument("--output", type=Path, default=Path("target/acceptance-report.json"))
+ parser.add_argument("--require-testcontainers", action="store_true")
+ args = parser.parse_args()
+ result = inspect(args.reports, json.loads(args.manifest.read_text(encoding="utf-8-sig")))
+ if args.require_testcontainers and result["environment"] != ["testcontainers"]:
+ result["passed"] = False
+ result["failures"].append("Docker/Testcontainers evidence required; external PostgreSQL is not equivalent")
+ result["manifestSha256"] = hashlib.sha256(args.manifest.read_bytes()).hexdigest()
+ jar = args.reports / "orquestador-nmp-0.1.0-SNAPSHOT.jar"
+ if jar.is_file():
+ result["jarSha256"] = hashlib.sha256(jar.read_bytes()).hexdigest()
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ args.output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
+ print(json.dumps({k: result[k] for k in ("passed", "environment", "tests", "failures")}))
+ return 0 if result["passed"] else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/specs/ACCEPTANCE.md b/specs/ACCEPTANCE.md
new file mode 100644
index 0000000..f190023
--- /dev/null
+++ b/specs/ACCEPTANCE.md
@@ -0,0 +1,77 @@
+# Verificación integral T16
+
+La fuente ejecutable es [acceptance-cases.json](acceptance-cases.json). Cada caso
+A01–A20 referencia métodos de prueba concretos. El checker exige ejecuciones reales
+en XML Surefire/Failsafe, rechaza cualquier fallo/error/skip y rechaza métodos
+faltantes. No considera suficiente que exista el código fuente de una prueba.
+
+## Ejecución con Docker/Testcontainers (criterio de cierre)
+
+Requiere JDK 21, Python 3 y motor Docker operativo:
+
+```sh
+docker info
+sh ./mvnw --batch-mode --no-transfer-progress clean verify
+python3 scripts/verify_acceptance.py --require-testcontainers
+```
+
+Windows: usar mvnw.cmd y python. La suite crea contenedores PostgreSQL 9.6.24
+temporales, JWT/JWKS locales, proveedores HTTP loopback y procesos JVM exclusivos
+de pruebas. No necesita PostgreSQL productivo ni Keycloak real. Failsafe no omite
+tests si Docker falta: falla. CI ubuntu-24.04 ejecuta estas mismas puertas, limita
+el job a 30 min y conserva reportes/manifest por 30 días incluso al fallar Maven.
+Una configuración de CI guardada no demuestra que el job remoto ya se ejecutó.
+
+## Verificación alternativa con PostgreSQL de pruebas
+
+```powershell
+# Definir MIGRATION_TEST_DB_URL/USER/PASSWORD en el entorno del proceso.
+# URL permitida por los fixtures: jdbc:postgresql://HOST:PORT/orquestador_test
+./mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external
+python scripts/verify_acceptance.py
+```
+
+Usa esquemas/roles temporales propios y los elimina al finalizar. Nunca usar una
+base de runtime. Los secretos no se escriben en reportes de aceptación. El checker
+infiere external-postgresql de las propiedades reales de Failsafe; si se agrega
+--require-testcontainers a esta evidencia, devuelve error. La corrida alternativa
+verifica comportamiento SQL/HTTP/JVM, pero no satisface por sí sola el requisito
+de reproducibilidad Docker de T16.
+
+Ejecutar siempre clean verify antes del checker: no combinar reportes de corridas
+parciales anteriores. acceptance-report.json guarda los casos, métodos/parametrizaciones
+ejecutadas, totales, entorno y hashes de manifest/JAR. Archivar XML y reporte juntos.
+El checker tiene pruebas propias: `python -m unittest discover -s scripts -p 'test_*.py'`.
+
+## Cobertura
+
+| Caso | Evidencia principal |
+| --- | --- |
+| A01 | DemoFlowsIT.creditBoundaries: cliente encontrado, persistencia y recorrido |
+| A02 | DemoFlowsIT.missingCustomerRemainsCreatedAndCanBeRetried: SUCCEEDED/CLIENTE_NO_ENCONTRADO |
+| A03 | DemoFlowsIT.customerApiFailureRetriesAndNeverBecomesCustomerNotFound: mock 503, tres intentos, FAILED |
+| A04 | DemoFlowsIT.creditBoundaries + DecisionEvaluatorTest: 71/70/61/60 y decimales |
+| A05 | DemoFlowsIT.invalidProviderScoreNeverDefaultsToManualReview |
+| A06 | DecisionEvaluatorTest UNIQUE + FlowSimulationTest.ambiguousDraftDecisionIsNotConvertedIntoDefault |
+| A07 | DemoFlowsIT.repeatedEventHasOneExecutionAndOneProviderCallEvenAfterCompletion |
+| A08 | EventAcceptanceIT.concurrentDifferentPayloadsConflictAndNumericCanonicalReplayIsStable |
+| A09 | EventAcceptanceIT.concurrentIdenticalAndDifferentKeysHaveOneAcceptance |
+| A10 | DemoFlowsIT.existingCreditKeepsV1WhileNewCreditRequiresLicense |
+| A11 | DocumentTasksIT: INVALID, documento ajeno/no conforme y referencia no reasignable |
+| A12 | RetryReconciliationIT: caídas reales de JVM; WorkerIT: checkpoint durable |
+| A13 | RetryReconciliationIT + WorkerIT: clave externa estable o conciliación |
+| A14 | WorkerIT: SKIP LOCKED, renovación y fencing de worker obsoleto |
+| A15 | EventAcceptanceIT, ObservabilityIT, SimulationIT: roles/ámbitos/propiedad |
+| A16 | FlowPublicationIT.databaseAndApplicationBothPreventPublishedMutation |
+| A17 | FlowPublicationIT: activación/retiro/asignación concurrentes |
+| A18 | DemoFlowTest y DemoFlowsIT: v1/v2 con mismo motor/binario; licencia en JSON/perfil |
+| A19 | WorkerIT y RetryReconciliationIT: rollback auditado y recuperación |
+| A20 | DocumentTasksIT.clientCannotSubmitProviderFieldsOrReplaceMetadata |
+
+Las carreras usan barreras/latches y bloqueos DB; los retries adelantan next_attempt_at
+en el esquema de pruebas sin esperar backoff real. CrashWorkerProcess usa Runtime.halt
+en JVM hija; el padre reconstruye el worker desde PostgreSQL. No es una prueba de
+reinicio de contenedor ni una garantía de exactly-once de un proveedor real.
+
+Pendiente histórico de T07 cerrado en implementación: [simulación](SIMULATION.md).
+T17 conserva empaquetado/demo desde checkout limpio y verificación de imagen Docker.
diff --git a/specs/ARCHITECTURE.md b/specs/ARCHITECTURE.md
index 30460cf..a3147c8 100644
--- a/specs/ARCHITECTURE.md
+++ b/specs/ARCHITECTURE.md
@@ -1,4 +1,4 @@
-# Arquitectura
+# Arquitectura
Un único módulo Maven, paquete raíz `com.organizacion.orchestrator`. La implementación sigue la especificación; esta entrega establece sus fronteras, no implementa todavía el motor.
@@ -28,4 +28,48 @@ Una sola ejecución activa por solicitud. La incertidumbre de un efecto externo
Publicación inmutable; instancias fijan flow_id. Activo se resuelve coherentemente en PostgreSQL. JSONB extiende datos; no permite scripts, clases, URLs o credenciales arbitrarios. Cambios de licencia con capacidades existentes no requieren columnas ni condiciones Java específicas.
-JWT se valida por firma, expiración, issuer y audiencia. Actualmente sólo se expone salud mínima. Roles, scope y propiedad se implementarán antes de los endpoints de negocio. Sin entidades abiertas durante llamadas HTTP (`open-in-view=false`).
+JWT se valida por firma, expiración, issuer y audiencia. T07 expone administración protegida y T08 creación/consulta de solicitudes. Roles, scope y propiedad se verifican antes de acceder a recursos; ambos grupos de rutas requieren habilitación explícita y runtime DB restringido. Sin entidades abiertas durante llamadas HTTP (`open-in-view=false`).
+
+## Dominio implementado en T04
+
+Consultar [modelo y puertos](DOMAIN_MODEL.md). El dominio y los puertos son Java
+puro; los records contienen snapshots inmutables. flow/api/ValidationReportResponse
+es un DTO separado con conversión explícita y prueba contra el esquema REST.
+Los adapters JPA/HTTP y servicios de aplicación se implementarán en sus tareas;
+ningún record del dominio es entidad JPA ni se expone automáticamente por REST.
+
+## Administración interna de flujos T06
+
+Consultar [validación y publicación](FLOW_PUBLICATION.md). FlowAdministration y sus
+puertos son independientes de infraestructura. PostgresFlowStore implementa el
+protocolo de locks/auditoría con JDBC transaccional (ADR-010). La exposición de
+controladores administrativos queda ligada a T07, con autenticación/autorización
+verificada; los contratos REST existentes no se modifican.
+
+
+## Solicitudes T08
+
+Requests y RequestStore mantienen la frontera pura de aplicación. PostgresRequestStore
+comparte DataSource/transacción con FlowStore, serializa claves idempotentes en
+PostgreSQL y confirma solicitud/auditoría/ledger en conjunto. RequestView es un DTO
+explícito; su consulta usa un snapshot SQL para instancia, flujo fijado y ejecución
+activa. Configuración de infraestructura común independiente del interruptor admin.
+Detalles y decisiones: REQUESTS.md, SECURITY.md y ADR-012.
+
+## Aceptación de eventos T09
+
+Events/EventStore y EventPolicy conservan aplicación/dominio puros. PostgresEventStore
+bloquea la solicitud autorizada y confirma ejecución, revisión y auditoría juntas.
+El replay lee estado actual antes de revisar la revisión esperada; las acciones
+permanecen sin ejecutar. La definición se obtiene por flow_id fijado, también tras
+retiro, sin invertir el orden de locks de publicación. Ver EVENTS.md y ADR-013.
+
+## Worker T10
+
+WorkerStore separa reclamación, renovación, carga/checkpoints y confirmación atómica.
+PostgresWorkerStore implementa fencing con tiempo DB. WorkerEngine resuelve mappings,
+reutiliza decisiones T05 y valida datos finales. WorkerLoop limita concurrencia y
+renueva durante acciones fuera de transacción. WorkerActions es el puerto para
+adapters T11; el fallback no inventa resultados. ExecutionController expone una
+proyección autorizada del estado. V3 conserva effect para recuperación segura.
+Ver WORKER.md y ADR-014.
diff --git a/specs/AVAILABLE_EVENTS.md b/specs/AVAILABLE_EVENTS.md
new file mode 100644
index 0000000..86a4318
--- /dev/null
+++ b/specs/AVAILABLE_EVENTS.md
@@ -0,0 +1,83 @@
+# Eventos disponibles y tareas — T13
+
+GET /api/v1/requests/{id}/available-events está habilitado con
+REQUEST_API_ENABLED=true. Retorna AvailableEvents del contrato existente:
+requestId, revision, events, blocked y, cuando aplica, blockedByExecutionId.
+Requiere JWT válido y propiedad/ámbito de la solicitud; FLOW_ADMIN no permite
+consultar recursos ajenos.
+
+Cada evento incluye su inputSchema publicado y, si hay una tarea asociada,
+metadatos ui de FORM o DOCUMENT_UPLOAD. No se crea una interfaz gráfica, un
+servidor de archivos ni otro motor de tareas. La aplicación cliente puede usar
+estos datos para mostrar su formulario/carga documental.
+
+## Selección y consistencia
+
+- Usa el flow_id fijado a la solicitud, incluso si está RETIRED o existe una nueva
+ versión activa para otras solicitudes.
+- Solo incluye transiciones del estado actual con un rol permitido del actor.
+ Los eventos de otros roles se omiten, sin exponer sus schemas o guardias.
+- PENDING, RUNNING, RETRY_WAIT y RECONCILIATION_REQUIRED ocultan todos los eventos
+ y tareas; blockedByExecutionId identifica la ejecución activa.
+- Una guardia evaluable con datos actuales de solicitud/actor que es falsa
+ devuelve GUARD_NOT_SATISFIED en blocked. Un error de evaluación devuelve
+ EVALUATION_ERROR, sin rutas, valores ni detalle interno.
+- Si cualquier parte de la guardia depende de event.data, se difiere la guardia
+ completa al POST. El evento permanece visible para capturar esos datos. Esto
+ también aplica a EXISTS/NOT: ausencia de payload en GET no significa que el
+ futuro evento carecerá del campo.
+
+Una sola sentencia SQL obtiene solicitud, definición y ejecución activa con una
+vista MVCC consistente, sin locks de fila. No llama proveedores. La respuesta es
+orientativa y puede quedar desactualizada inmediatamente: POST revalida siempre
+roles, revisión, estado, guardia y ausencia de otra ejecución.
+
+Cache-Control: no-store. No se emite ETag en este endpoint: disponibilidad cambia
+por roles y estado técnico, aunque revision no cambie. Usar revision de la respuesta
+para formar If-Match al enviar el evento, o volver a consultar GET de solicitud.
+El POST puede responder 409/412 si hubo una carrera.
+
+## Documentos
+
+El cliente envía documentId conforme al inputSchema. No envía MIME, tamaño,
+checksum, resultado antimalware, score o resultado de validación como autoridad.
+REGISTER_DOCUMENT obtiene los metadatos del servicio documental configurado y
+comprueba identidad, solicitud/ámbito, tipo, MIME, tamaño, checksum SHA-256 y
+scanStatus=CLEAN.
+
+DocumentMetadata centraliza la verificación pura. El worker aplica adicionalmente
+los límites más restrictivos del DOCUMENT_UPLOAD publicado antes de guardar
+referencia/checkpoint, y también al reutilizar un checkpoint. Ejemplo: si el
+catálogo admite 5 MiB y PNG/PDF, pero la tarea solo 1000 bytes/PDF, 1001 bytes o PNG
+se rechazan. La referencia y su checkpoint mantienen la transacción/fencing T11.
+Los registros existentes se reutilizan únicamente si todos sus metadatos coinciden.
+
+MIME real, checksum y análisis antimalware provienen del servicio documental
+confiable. El orquestador no descarga bytes para recalcularlos ni ejecuta un
+antivirus local. INFECTED/PENDING o metadatos inválidos no generan referencia ni
+se convierten en un resultado de negocio VALID/INVALID. Sin proveedor configurado
+no se fabrica éxito; los mocks existen únicamente en las pruebas.
+
+DOCUMENTO_VALIDA es una acción posterior independiente. INVALID técnicamente
+válido puede producir SUCCEEDED/DOCUMENTO_INVALIDO y mantener el mismo estado/tarea;
+las razones quedan en los datos de solicitud mediante mappings explícitos del
+flujo. GET de solicitud expone esos datos autorizados. No se agregan campos de
+razón libres al contrato de available-events.
+
+Un timeout/503 del validador produce reintentos técnicos y bloquea eventos; al
+agotarlos de forma segura, FAILED conserva el estado previo y permite volver a
+intentar. No se inventa INVALID ni se sobrescribe la última validación de negocio.
+El checkpoint documental previo se conserva y no repite la consulta ya exitosa.
+
+## Verificación
+
+AvailableEventPolicyTest verifica selección pura; AvailableEventsIT comprueba
+HTTP/JWT/PostgreSQL, versiones, roles, guardias y lectura frente a escritura
+concurrente. DocumentTasksIT recorre registro→validación→decisión→tarea con HTTP
+mock y PostgreSQL real: INVALID→nuevo evento VALID, referencias sin duplicar,
+documentos ajenos, MIME/tamaño de tarea, checksum/antimalware, error técnico y
+rechazo de payloads con datos de proveedor (A11/A20).
+
+Ejecutar mvnw.cmd clean verify -Pmigration-tests-external con credenciales de
+orquestador_test solo en MIGRATION_TEST_DB_*. Sin ese perfil, verify requiere
+Docker/Testcontainers. No se desplegó ni se usaron documentos o proveedores reales.
diff --git a/specs/DECISIONS.md b/specs/DECISIONS.md
index 42065a7..29cd5b8 100644
--- a/specs/DECISIONS.md
+++ b/specs/DECISIONS.md
@@ -82,3 +82,437 @@ Flyway 12.4.0 inspeccionado admite desde PostgreSQL 9.0 y la validación real so
Verificación: clean verify con Java 21 (92 tests) y postgres96-check (1 prueba de integración compuesta). La prueba inicia el servicio local con pool read-only, Flyway automático desactivado, SQL init desactivado y validación Hibernate; después invoca exclusivamente Flyway.validate() con createSchemas=false. No ejecuta migrate/create/clean ni modifica la instancia externa. La primera propuesta de arranque fue rechazada por la revisión automática por riesgo de migraciones; se usó esta alternativa explícitamente de sólo lectura, aprobada y ejecutada.
Fuentes: [dialectos y módulo comunitario Hibernate](https://docs.hibernate.org/stable/core/dialect/), [versiones de bases de datos Flyway](https://documentation.red-gate.com/fd/supported-databases-and-versions-143754067.html). La compatibilidad se comprobó con las dependencias reales del proyecto, no sólo con la documentación.
+
+## ADR-007 — Modelo persistente y pruebas aisladas T03 (2026-10-26)
+
+Decisión: implementar V1/V2 compatibles con PostgreSQL 9.6 y usar el esquema
+orchestrator para tablas e historial Flyway. El arquetipo anterior no tenía
+migraciones de negocio; si un entorno contiene historial real en public, requiere
+un traslado explícito antes de actualizar, nunca baseline/repair automático.
+Separar credenciales del migrador y runtime mediante DB_MIGRATION_*; conservar
+fallback a DB_* para el entorno de desarrollo existente. Los roles se provisionan
+externamente y grant_runtime_access concede permisos a un rol existente.
+
+La ausencia de Docker local se resuelve ejecutando las mismas pruebas sobre la
+base orquestador_test cuya creación confirmó el usuario. Cada ejecución crea y
+elimina su esquema t03_, sin tocar orquestador ni esquemas compartidos. La
+prueba de permisos crea un rol NOLOGIN y revierte su transacción. La ruta normal
+permanece en Testcontainers postgres:9.6.24 para CI; no se ejecutó localmente por
+falta de Docker. La evidencia real corresponde a PostgreSQL 9.6.2 externo.
+
+Se rechazó usar H2 o cerrar por compilación: se probaron FKs, JSONB, historial
+inmutable, permisos, migraciones desde cero, rollback de DDL, una carrera real de
+inserciones y FOR UPDATE SKIP LOCKED con conexiones independientes. Los triggers
+protegen integridad de almacenamiento; T06/T09/T10/T12 aún deben implementar
+validación semántica, auditoría de negocio, aceptación, fencing y conciliación.
+No se cambian contratos REST ni se declaran cumplidos A01–A20 por estas pruebas.
+
+## ADR-008 — Contratos internos inmutables (2026-10-26)
+
+T04 utiliza records y variantes selladas de Java 21. Se elige una representación
+JSON propia e inmutable con BigDecimal, NullValue explícito y Lookup.Missing
+separado, en lugar de exponer JsonNode mutable o Map en los puertos.
+Esto mantiene snapshots confiables y permite compilar dominio/puertos sin librerías
+externas. Los adapters futuros deberán convertir JSON y persistencia explícitamente.
+
+FlowDefinition modela configuración; FlowVersion modela su metadata persistida.
+Las identidades FlowId/RequestId/ExecutionId/ActionExecutionId impiden intercambios
+accidentales de UUID. Rutas, AST y acciones son variantes tipadas; estados/eventos
+siguen siendo claves configurables. El catálogo confiable aporta clasificación
+de efectos por separado, nunca mediante configuración editable del flujo.
+
+Errores técnicos y resultados de negocio tienen variantes distintas. Los puertos
+mínimos se declaran sin implementaciones simuladas; T05/T06/T11 implementarán sus
+comportamientos. El DTO de validación convierte el modelo al contrato publicado
+sin exponer detalles internos. No se modifican DSL, OpenAPI, SQL, idempotencia,
+versionado o política de autorización. Pruebas de arquitectura y de esquema
+protegen estas fronteras. Ver DOMAIN_MODEL.md para invariantes y límites.
+
+## ADR-009 — Evaluación pura y presupuestos de AST/datos (2026-10-26)
+
+T05 implementa el puerto sin dependencias externas ni evaluación de expresiones
+libres. Los once operadores usan JSON tipado; BigDecimal.compareTo evita coerción
+y diferencias por escala. La ausencia es un error salvo EXISTS; null solo participa
+en EQ/NEQ. Se concreta igualdad estructural para arrays/objetos y homogeneidad total
+de IN, sin ocultar tipos inválidos tras una coincidencia temprana.
+
+El preflight iterativo limita la definición completa antes de aplicar cortocircuito:
+profundidad 20 y 1000 nodos, incluyendo expresiones path/literal. Los límites pueden
+reducirse por construcción. Se limita además IN a maxNodes elementos y la igualdad
+estructural a maxNodes pares por comparación, para no recorrer datos arbitrariamente
+grandes aunque el AST tenga pocas hojas. Exceso retorna EVALUATION_ERROR. Esta
+restricción operativa queda explícita y no cambia schemas/SQL ni admite un default
+como sustituto de un error. Límites de bytes de recepción quedan en T06/T09.
+
+Alternativas descartadas: recursión durante validación de profundidad (podría
+desbordar antes del rechazo), validación solo de ramas ejecutadas (permitiría omitir
+límites), igualdad BigDecimal.equals (depende de escala), coerciones y evaluación
+arbitraria de scripts. IntervalOverlapAnalyzer analiza solo intervalos simples;
+la ambigüedad UNIQUE sigue comprobándose al ejecutar. No promete resolver SAT.
+
+La evaluación conserva IDs de reglas para auditoría futura y snapshots intactos.
+No se modifican las reglas de idempotencia, seguridad ni versionado. Detalle de
+operadores, alcance de A04–A06 y conteo de límites: DECISION_EVALUATOR.md.
+
+## ADR-010 — Publicación transaccional y exposición administrativa (2026-10-26)
+
+T06 implementa un servicio interno de administración y un catálogo inmutable
+proporcionado por el despliegue. No incluye controladores ni un catálogo ficticio
+predeterminado. Aunque A16 menciona API, su evidencia en esta fase se obtiene en
+la API Java de aplicación y en PostgreSQL; las rutas REST y la comprobación JWT
+se realizan en T07/T16. Esta secuencia resuelve la exigencia explícita de seguridad
+antes de exposición; no se da por aprobada la prueba HTTP que aún no existe.
+
+Desviación del uso propuesto de JPA para agregados: las operaciones administrativas
+se implementan con el JDBC ya incluido en el stack y TransactionTemplate, detrás
+de FlowStore. El motivo es mantener explícitos los bloqueos flujo/tipo, revalidación
+de referencias, JSON canónico y auditoría en una única transacción sin flush implícito.
+Alternativa: entidades JPA más queries nativas y control manual de flush; añade dos
+modelos de escritura al mismo protocolo sin una necesidad actual. No se elimina JPA
+ni se reemplaza el stack; otros agregados podrán usarlo sin cambiar los contratos.
+
+Activación y asignación bloquean flujo antes que tipo; retiro sigue ese orden y
+limpia solo referencias que todavía apuntan a la versión. Incrementa revision de
+los tipos desactivados y audita sus claves. Instancias existentes permanecen fijadas.
+T08 debe completar creación/idempotencia/auditoría en la misma transacción; el puerto
+no debe usarse como lectura desconectada seguida de una inserción posterior.
+
+La canonicalización es propia, determinista, con objetos ordenados, arrays ordenados
+y números BigDecimal normalizados; no se anuncia RFC 8785. Defaults y contratos se
+resuelven antes del hash. Entradas iniciales/de evento cerradas y protección de rutas
+escritas desde results impiden confiar en campos de proveedor recibidos del cliente.
+La validación estática de tipos no sustituye validación de valores en runtime.
+
+networknt 1.5.9 se reutiliza en runtime, con Jackson 2 aislado en infraestructura.
+Schemas se empaquetan desde los archivos contractuales existentes. No cambian las
+migraciones, idempotencia ni credenciales. Las pruebas reales usan exclusivamente
+orquestador_test y esquemas temporales propios conforme a la autorización vigente.
+Detalles, límites y protocolos: FLOW_PUBLICATION.md.
+
+## ADR-011 — Identidad y autorización T07 (2026-10-27)
+
+Aceptado para implementación local verificable. JWT RS256 validado por JWKS, issuer,
+audience y exp; sub y un ámbito de negocio único son obligatorios. Se toma
+business_scope por defecto, configurable con JWT_BUSINESS_SCOPE_CLAIM mientras se
+confirma el claim real de controlms. OAuth scope no sustituye el ámbito ni concede
+roles. Se admiten roles del realm y del cliente identificado por JWT_AUDIENCE.
+Alternativa descartada: confiar en actor/scope enviados en el comando o usar la
+clave pública estática sin posibilidad de actualización por JWKS.
+
+Los recursos exigen propietario Y ámbito, con 404 para ausencia o acceso ajeno.
+No existe bypass implícito FLOW_ADMIN. EXECUTION_OPERATOR tiene una puerta separada
+de conciliación dentro de su ámbito, sin requerir ser propietario; la operación
+funcional y evidencia permanecen en T12. Alternativa descartada: cualquier usuario
+del ámbito puede acceder a todas las solicitudes, pues ampliaría acceso sin una
+regla de negocio explícita.
+
+El catálogo de flujos/tipos es global según el modelo actual. Su administración
+requiere rol técnico y un ámbito en ADMIN_BUSINESS_SCOPES, lista vacía por defecto.
+No se inventa tenancy para flow_definition. FLOW_ADMIN_ENABLED=false conserva las
+rutas cerradas hasta que el operador configure catálogo, ámbito y credenciales DB
+separadas. Con administración habilitada, un verificador rechaza al runtime elevado
+y permisos de mutación de historiales; Flyway usa su propio usuario propietario.
+No se cambia automáticamente la cuenta pgadmin existente ni permisos globales.
+
+Las seis operaciones administrativas T06 se exponen en T07. Simulación sigue pendiente
+para T14 y conciliación para T12. A15 se verifica con la puerta de autorización real,
+JWT firmado, recursos persistidos y un controlador exclusivo de test no distribuido;
+las rutas funcionales posteriores deben reutilizarla y T16 repetirá la aceptación
+integral. A16 se comprueba ahora por HTTP. Detalles: SECURITY.md.
+
+## ADR-012 — Creación idempotente y snapshot T08 (2026-10-27)
+
+La clave de creación se particiona por sujeto/ámbito y el hash incluye typeKey/data
+canónicos, usando la semántica numérica T06. Se serializa con advisory lock
+transaccional PostgreSQL de 64 bits y búsqueda/restricción única por identidad
+completa. Una colisión del lock no mezcla identidades, solo retrasa transacciones.
+Alternativas: mutex JVM (no protege varias instancias), reserva anticipada de ledger
+(exigiría respuesta/solicitud incompletas o una migración adicional) e intentar insertar
+sin serializar (recuperación de conflicto más compleja después de abortar transacción).
+
+La respuesta original queda durable en el ledger y se reproduce con 201/Location/ETag
+originales; GET devuelve el estado actual. Se verifica acceso actual antes del replay.
+La resolución de activo y validación por schema ocurren después de comprobar replay,
+para que cambios posteriores de catálogo no invaliden una creación ya aceptada. La
+clave distinta de contenido produce IDEMPOTENCY_CONFLICT. Fallos no consumen la clave.
+
+Solicitud, REQUEST_CREATED y ledger se guardan en una transacción READ COMMITTED,
+con una misma DataSource que FlowStore. Se conserva orden flujo/tipo de T06. El cambio
+de activo detectado durante asignación retorna 409 INVALID_FLOW_STATE reintentable,
+sin registro parcial. El aislamiento de usuario/scope se aplica a cada consulta.
+
+Se extiende el adapter JDBC tras puertos de aplicación puros, en continuidad con
+ADR-010, para compartir bloqueos, JSONB y commit con FlowStore. No se añade JPA para
+un segundo modelo de escritura del mismo protocolo ni se altera el stack existente.
+No hay nuevas migraciones: V1/V2 ya contienen ledger, FKs, auditoría y permisos.
+
+REQUEST_API_ENABLED es independiente de administración. Ambos requieren runtime DB
+restringido; PersistenceAccessConfiguration factoriza este guard y los puertos comunes.
+Creación exige identidad JWT válida, sin inventar rol nuevo ni ACL por tipo; los roles
+de negocio se revalidan en eventos T09. Ver REQUESTS.md y contrato actualizado.
+
+## ADR-013 — Aceptación durable y revisión T09 (2026-10-27)
+
+Se mantiene el protocolo de la sección 8.1: bloquear solicitud autorizada, buscar
+idempotencia antes de revisión, validar transición/rol/schema/guardia, insertar
+PENDING, incrementar revisión y auditar en una transacción READ COMMITTED. El índice
+único de ejecución activa y la clave request_id/idempotency_key existentes son
+respaldo SQL; no hay mutex en memoria ni HTTP externo dentro de aceptación.
+
+accepted_revision guarda la revisión posterior a aceptación, If-Match + 1. El
+worker deberá comparar esa revisión al confirmar; no es una revisión adicional
+independiente. Hash SHA-256 de evento/data/revisión esperada canónicos, con misma
+semántica decimal de T06/T08. El replay verifica acceso actual y actor original,
+retorna status actual y no vuelve a exigir el rol/guardia de la aceptación previa:
+no está autorizando un nuevo efecto. Otra revisión esperada con la misma clave
+es otro comando y produce IDEMPOTENCY_CONFLICT.
+
+La definición se lee por flow_id fijado, incluyendo RETIRED, sin bloquear el flujo
+ni consultar el tipo activo. No se altera estado/data de negocio al aceptar. Guardias
+usan results vacío y los snapshots confiables de solicitud/evento/actor; errores de
+evaluación no se convierten a false. No se guarda JWT completo, solo atributos
+necesarios de identidad. Persistencia JDBC tras puertos puros continúa ADR-010/012.
+
+Alternativas descartadas: procesar acciones antes del 202 (mezcla transacción/HTTP),
+revalidar revisión antes del replay (rompe reintento perdido), devolver un éxito de
+negocio al aceptar y almacenar tokens para reintentos. No hay migración nueva:
+V1/V2 cubren tabla, índices, FKs, permisos e inmutabilidad de auditoría.
+
+Location conserva el contrato de ejecución; su consulta GET se incorpora como
+subtarea explícita T10 para seguimiento del worker. En T09 solo se implementa POST
+de aceptación y GET de solicitud T08 permite ver activeExecutionId; no se anuncia
+procesamiento ni consulta de ejecución implementados. Ver EVENTS.md.
+
+## ADR-014 — Worker, fencing y recuperación inicial T10 (2026-10-27)
+
+WorkerStore reclama PENDING/RETRY_WAIT vencidos y RUNNING expirados con SKIP LOCKED,
+tiempo DB y token creciente. Los executors locales solo despachan capacidad libre;
+PostgreSQL conserva cola, leases y checkpoints. Toda mutación exige owner/token
+vigentes; checkpoints bloquean ejecución, finalización/fallo bloquean solicitud y
+luego ejecución. Se comprueban revisión/estado al cargar y al confirmar. Nunca se
+mantiene una transacción abierta mientras el adapter ejecuta una acción.
+
+V3 agrega effect al checkpoint, obtenido del adapter confiable y comparado al
+reutilizarlo. Alternativa descartada: inferir seguridad desde el JSON editable o
+suponer que un checkpoint antiguo era read-only. Los anteriores reciben clasificación
+conservadora NON_IDEMPOTENT_WRITE. Hash/clave/effect son consistentes entre reintentos.
+RUNNING/UNKNOWN no idempotentes requieren conciliación. Éxitos se reutilizan; cambios
+de hash/effect se rechazan. Escrituras previas/potenciales impiden liberar como FAILED.
+
+El procesamiento secuencial resuelve inputs, valida contratos, guarda checkpoint,
+evalúa decisiones y aplica updates sobre copia. Commit final incluye datos/estado,
+revisión, resultado y auditoría con reglas/acciones. Errores DB conservan leases y
+checkpoints para recuperar; no se transforman en éxito o en liberación incierta.
+JSON Pointer permite actualizar objetos y posiciones existentes de arrays, sin
+crear arrays dispersos. Se valida schema/256 KiB antes de confirmar. No hay claims
+de exactly-once externo; la clave executionId:actionId solo funciona si el proveedor
+respeta su contrato de idempotencia.
+
+WORKER_ENABLED independiente de API/admin, deshabilitado por defecto. Registro
+productivo sin adapters hasta T11: ausencia produce INTEGRATION_UNAVAILABLE, nunca
+mock de éxito. Flujos sin acciones ya pueden completarse. T12 conserva la política
+completa de retry/backoff/Retry-After/conciliación; T10 implementa recuperación de
+caídas y reconoce RETRY_WAIT existente. TechnicalFailure se termina de manera segura
+sin anunciar retry HTTP aún. GET de ejecución ya se implementa con owner/scope y
+proyección de respuesta, resolviendo la subtarea agregada en ADR-013.
+
+Pruebas: PostgreSQL real, dos reclamantes, checkpoints, retorno tardío y heartbeats;
+adapters exclusivamente de test. SimulatedCrash y reconstrucción de repositorio no
+se presentan como reinicio de JVM. WORKER.md documenta configuración y límites.
+
+## ADR-015 — T11: transporte acotado y contratos de despliegue (2026-10-27)
+
+Se conserva Spring Boot y sus beans, usando HttpClient del JDK 21 dentro del
+adapter HTTP en lugar del cliente Spring propuesto en la especificación. Motivo:
+controlar cancelación y límite de bytes antes de acumular una respuesta, además
+de un deadline que incluya el cuerpo completo. No se agrega otra librería.
+Alternativas: cliente Spring síncrono con lectura InputStream (requiere controlar
+por separado el deadline de lectura) o WebClient (agrega stack reactivo).
+La API BodySubscriber documenta cuándo se completa el cuerpo y cómo cancelar la
+suscripción: [documentación JDK 21](https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpResponse.BodySubscriber.html).
+BoundedHttp usa un subscriber limitado y espera finita del futuro; las pruebas
+incluyen headers inmediatos con cuerpo demorado y payload excesivo.
+
+La ruta es configuración confiable, ligada a tupla exacta de contrato/perfil.
+No hay URLs, secretos ni selección de métodos HTTP en el DSL. El protocolo local
+propuesto usa POST JSON y bearer opcional por referencia a variable de entorno.
+No se inventa compatibilidad con proveedores existentes: ejemplos y mocks
+definen el contrato que deberá confirmarse o adaptarse antes de habilitar rutas.
+Nuevas versiones conviven; no cambiar el significado de una versión usada.
+
+REGISTER_DOCUMENT usa DOCUMENTS_METADATA/1 como protocolo fijo del handler,
+porque el DSL de esta acción no tiene campos de versión. Cambiar ese protocolo
+requiere versionar el handler/DSL mediante otra decisión, nunca seguir un default
+mutable. Guarda referencia y checkpoint juntos bajo fencing. Su efecto remoto es
+READ_ONLY; el registro local es idempotente y no sube archivos. T13 completa la
+exposición documental. NO_OP permanece solo en simulación.
+
+A03 se divide explícitamente: T11 acredita transporte, clasificación y ausencia
+de falsos negativos; T12 implementa programación de reintentos/backoff/Retry-After.
+No se anuncia A03 integral terminado hasta T12/T16. No cambian las claves
+idempotentes, versiones publicadas o autorización. Compose conserva un servicio
+y PostgreSQL externo; sin despliegue ni llamadas reales.
+
+## ADR-016 — T12: reintento durable y resolución atómica (2026-10-27)
+
+Se programa RETRY_WAIT con reloj PostgreSQL en la misma transacción que el estado
+del intento y RETRY_SCHEDULED, bajo fencing. No se duerme dentro del worker ni de
+una transacción. Se conserva el límite publicado por acción, máximo tres intentos
+totales; backoff exponencial con jitter 80–120 %, limitado por maxBackoff, y
+Retry-After numérico/fecha HTTP con techo de 120 s.
+
+READ_ONLY e IDEMPOTENT_WRITE pueden reintentarse tras fallo transitorio. UNKNOWN y
+RUNNING recuperado de NON_IDEMPOTENT_WRITE se concilian antes de evaluar agotamiento:
+alcanzar el límite no demuestra ausencia de efecto. Un intento FAILED de escritura
+idempotente agotada también conserva incertidumbre. Un resultado normalizado
+negativo sigue siendo SUCCEEDED. Claves externas, hashes de entrada y versiones
+publicadas no cambian. El hint Retry-After se añade al resultado técnico interno,
+sin alterar el contrato público de ejecución.
+
+La evidencia de resolve es una declaración del operador autorizado con referencia
+y razón; no existe servicio real configurado para verificarla automáticamente.
+No fabricar una consulta de estado ni afirmar verificación remota. El operador
+EXECUTION_OPERATOR accede por ámbito, incluso si no es propietario; además requiere
+el interruptor administrativo y ADMIN_BUSINESS_SCOPES existentes.
+
+RESUME valida contratos fijados, efectos persistidos y cobertura de todos los
+checkpoints no exitosos. No crea acciones no iniciadas, cambia resultados exitosos
+ni inserta referencias documentales manuales. Reencola PENDING; el worker decide
+el destino con el flujo publicado. CLOSE_WITHOUT_TRANSITION conserva negocio y
+exige declaración ABSENT_VERIFIED/COMPENSATED_VERIFIED; ausencia contradicha por
+una escritura SUCCEEDED durable se rechaza. No se agregan cambios de estado manuales.
+
+Se bloquea solicitud antes que ejecución. Checkpoints, ledger append-only,
+auditoría y reencolado/cierre son atómicos; el fencing_token aumenta al resolver.
+Replay se busca antes de comprobar estado actual, exige mismo comando canónico y
+sujeto operador, y devuelve respuesta original. Otro sujeto con igual clave no
+hereda el replay. El esquema V1 ya soporta esta semántica: no se necesita migración.
+API informa 409 para estado no conciliable y 412 si se detecta revisión/estado
+de solicitud incoherentes al reanudar; OpenAPI refleja ambas respuestas.
+
+Alternativas descartadas: reintentar toda la transición (duplica efectos previos),
+liberar al agotar intentos sin considerar escrituras (pierde incertidumbre) y
+permitir seleccionar destino desde resolve (evade reglas publicadas). La prueba
+de caída usa una JVM hija real terminada con Runtime.halt; proveedor HTTP sintético
+con efecto lógico por clave. No prueba un proveedor real ni un despliegue productivo.
+
+## ADR-017 — T13: disponibilidad orientativa y límites documentales publicados (2026-10-27)
+
+available-events es una proyección de backend del flujo fijado a la solicitud.
+No se implementa interfaz gráfica. Una sentencia SQL obtiene revisión, estado,
+datos, definición inmutable y ejecución activa en el mismo snapshot MVCC; no se
+bloquean filas ni se consulta disponibilidad de proveedores. Se conserva acceso
+por propietario/ámbito, sin bypass FLOW_ADMIN, y se omiten transiciones de otros roles.
+
+Con ejecución activa se ocultan todos los eventos y metadatos. Guardias que solo
+usan solicitud/actor se evalúan; falso y error se proyectan como códigos genéricos.
+Guardias con cualquier dependencia de event.data se difieren completas al POST.
+Evaluarlas con un objeto vacío produciría falsos bloqueos, especialmente en
+EXISTS/NOT. No se promete que un evento anunciado será aceptado: POST revalida
+revisión, rol, guardia, estado e inactividad.
+
+No-store y ausencia de ETag en available-events: roles y estado técnico pueden
+cambiar sin variar revision. Un ETag basado solo en revisión podría producir un
+304 incorrecto. Se devuelve revision en el cuerpo para la precondición del POST.
+No se cambia el contrato/ETag de GET de solicitud.
+
+DocumentMetadata extrae la verificación pura existente de T11. Se mantiene la
+autoridad del servicio documental para MIME real/checksum/antimalware, sin descargar
+archivos ni simular éxito productivo. El worker ahora verifica también MIME/tamaño
+del DOCUMENT_UPLOAD publicado antes de guardar referencia/checkpoint y al
+reutilizarlo. T11 aplicaba únicamente el límite general del catálogo; esto cerraba
+insuficientemente tareas que anunciaban restricciones más estrictas. Se aplican
+ambas políticas, no se amplía la autorización.
+
+El fixture documental separa documentId de entrada y registeredDocumentId verificado,
+con validation protegida como salida del proveedor. No se relaja la regla de
+publicación que impide escribir resultados desde datos del cliente. Los enums
+status/scanStatus del catálogo de ejemplo agregan type:string explícito para el
+análisis estático; sus valores aceptados no cambian.
+
+INVALID de DOCUMENTO_VALIDA conserva tarea/estado con razón mapeada a datos de
+solicitud. Fallo técnico conserva los datos previos y sigue T12, sin fabricar
+INVALID. Referencias y checkpoints conservan atomicidad/fencing T11; no se agrega
+tabla ni endpoint de subida/descarga de archivos. Compose mantiene solo el servicio
+con PostgreSQL externo. Proveedor documental real y antivirus real siguen fuera
+de la evidencia local con mocks.
+
+## ADR-018 — Demostración por configuración y segundo tipo (2026-10-28)
+
+T14 entrega crédito v1/v2 y compra v1 como definiciones completas, sin modificar
+src/main ni columnas. Se amplía solo el catálogo de ejemplo con INE/ESTADO_CUENTA
+versión 1; LICENCIA_CONDUCIR ya existe. Todos los contratos y perfiles quedan
+explícitos en JSON. Límites documentales: 5 MiB y MIME según catálogo/tarea.
+
+La especificación no define políticas de compra. El ejemplo usa consulta de cliente
+y confirmación booleana, con finales confirmada/cancelada; no cobra ni reserva.
+Alternativa descartada: inventar una integración de pagos o reglas comerciales.
+Crédito exige una referencia de consentimiento; no afirma validar consentimiento
+real. Los cuatro proveedores HTTP son fixtures sintéticos exclusivos de pruebas;
+no se empaquetan en el servicio ni se activa configuración de demo por defecto.
+
+Se verifica la demo mediante API operativa y worker, con esquema temporal PostgreSQL,
+en vez de presentar una simulación como recorrido persistente. El endpoint puro
+/admin/flows/{id}/simulate sigue pendiente (anotado desde T07); no forma parte de
+la evidencia A10/A18 ni se declara implementado. Se registra explícitamente para
+la revisión integral T16 antes de la entrega final T17.
+
+## ADR-019 — Historial y observabilidad operativa (2026-10-28)
+
+El historial se proyecta desde audit_entry append-only con keyset (created_at,id),
+límite 100 y filtros kind/execution/from/to. ResourceAccess autoriza propietario y
+ámbito en cada página, sin bypass administrativo. El cursor codifica posición y
+huella de solicitud/filtros; no es un token de acceso ni una firma criptográfica.
+Se elige estabilidad entre reinicios/réplicas sin añadir secretos para una posición
+que no amplía acceso. Alternativa: cursores firmados; no aporta autorización al
+recurso y exige rotación compartida. Navegación no promete snapshot ante commits
+concurrentes tardíos. La consulta tiene timeout SQL de 5 s.
+
+Se entrega lista permitida de detail; evidencia de conciliación y payloads no se
+proyectan. Nuevas transiciones agregan reasonCodes presentes en resultados verificados
+a la misma escritura auditada; no se alteran auditorías existentes. Actor se entrega
+al propietario autorizado. Sin nueva migración ni modificación de semántica de
+idempotencia/versiones. Retención no borra datos sin política de organización.
+
+Actuator metrics se habilita y protege con EXECUTION_OPERATOR + ámbito de operación
+configurado, independiente de edición de flujos. Otros actuators se deniegan salvo
+salud. Micrometer ya transitivo: sin dependencia nueva, Prometheus o servicio extra.
+Contadores de comandos describen respuestas HTTP incluyendo replays; no representan
+exactamente commits nuevos. Timers usan integrationKey confiable y resultado fijo;
+estados DB son gauges agregados sin IDs/sujetos/ámbitos, con muestra compartida de
+5 s y timeout SQL 3 s. Fallos dan available=0 y NaN, nunca un cero de cola ficticio.
+
+Logs propios usan campos permitidos y códigos; decisiones/razones quedan en auditoría.
+Un log de acción exitosa no equivale a commit; phase=committed se emite después del
+commit durable. Correlación HTTP queda ligada a executionId por EVENT_ACCEPTED.
+WorkerShutdown amplía el drenaje existente con presupuesto configurable 1–20 s y
+espera adicional de 1 s tras interrupción. Mantiene renovaciones durante el drenaje,
+fencing/checkpoints y recuperación de incertidumbre. No se afirma cancelar efectos
+externos no cooperativos ni probar Docker productivo mediante fixtures locales.
+
+## ADR-020 — Simulación y evidencia ejecutable de aceptación (2026-10-28)
+
+T16 cierra el endpoint de simulación pendiente desde T07. SimulationController exige
+la seguridad administrativa existente; actor/results del body son fixtures. Se lee
+el flujo y se libera el lock antes de evaluar. FlowSimulation no depende de HTTP,
+secretos, worker actions o puertos de escritura. Valida schemas, versiones/perfiles,
+roles/guardias y mappings; NO_OP usa output configurado solo en simulación.
+
+Se extrae mecánicamente la proyección final del worker a TransitionEvaluator,
+compartiendo decisiones/updates/validación final/reglas y razones. Alternativa
+rechazada: duplicar el evaluador en el endpoint, con riesgo de divergencia. La
+extracción no modifica idempotencia, leases, efectos, transacciones o semántica
+numérica. Simulación permite draft ambiguo para devolver AMBIGUOUS_DECISION, pero
+publicación conserva su validación estricta. No crea auditoría ni registros de
+negocio; es evaluación de fixtures, no evidencia de proveedor/documento real.
+
+acceptance-cases.json liga A01–A20 a métodos concretos. verify_acceptance.py inspecciona
+XML real y falla por ausencia/error/skip; registra entorno y hashes. CI exige
+Testcontainers y conserva artefactos 30 días. FailIfNoTests se habilita en ambos
+plugins Maven. No se cambian versiones de stack ni se incorporan librerías nuevas.
+
+Docker no fue localizado en PATH, servicio o instalación estándar de este equipo.
+Se solicitó un motor autorizado mientras se completa verificación con orquestador_test.
+Una corrida externa aprobada no se rotula Docker ni satisface su criterio: T16 no
+puede declararse DONE hasta verificar Testcontainers o una modificación explícita
+del criterio por el usuario. CI configurado tampoco equivale a CI ejecutado.
diff --git a/specs/DECISION_EVALUATOR.md b/specs/DECISION_EVALUATOR.md
new file mode 100644
index 0000000..a29d08a
--- /dev/null
+++ b/specs/DECISION_EVALUATOR.md
@@ -0,0 +1,92 @@
+# Evaluador de decisiones (T05)
+
+`PureDecisionEvaluator` implementa `DecisionEvaluator` sin Spring, I/O ni
+persistencia. Es reutilizable entre workers; no guarda contexto ni resultados
+entre llamadas. Devuelve `Selected(output, matchedRuleIds)` o `Failure(code)`.
+`ConditionEvaluator` permite evaluar guardias: devuelve boolean o lanza
+DomainException con EVALUATION_ERROR. No convierte errores en condiciones falsas.
+
+## Semántica ejecutable
+
+- EQ/NEQ comparan tipos JSON compatibles; números mediante BigDecimal.compareTo,
+ sin depender de la escala. No hay conversión de strings a números o fechas.
+- Null es un valor presente. Solo EQ/NEQ lo comparan: null EQ null es true,
+ null EQ un valor no nulo es false. Una ruta ausente produce EVALUATION_ERROR
+ en cualquier comparación, incluso si el otro operando es null.
+- EQ/NEQ admiten objetos y arrays estructurales, conservando el orden del array
+ e ignorando el orden de claves del objeto. Valores anidados con tipos
+ incompatibles producen error; formas/tamaños distintos producen desigualdad.
+- GT/GTE/LT/LTE requieren dos números. No existe orden implícito de strings,
+ fechas, booleanos, objetos o null.
+- IN requiere array homogéneo y un operando izquierdo compatible con sus elementos.
+ Revisa toda la lista antes de buscar: una coincidencia temprana no oculta tipos
+ mezclados. Null no participa en IN. Una lista vacía retorna false para un valor
+ no nulo. La igualdad numérica tampoco depende de escala dentro de IN.
+- EXISTS retorna false solo para un path sin valor; un null explícito sí existe.
+ AND/OR cortocircuitan en orden; NOT invierte el resultado y propaga errores.
+- UNIQUE examina reglas hasta detectar una segunda coincidencia, que retorna
+ AMBIGUOUS_DECISION incluso si ambas salidas son iguales. Si ninguna coincide,
+ usa defaultOutput. Un error previo a decidir el resultado no usa el default.
+- FIRST se habilita únicamente con hitPolicy=FIRST y selecciona la primera regla
+ coincidente, sin evaluar los datos de reglas posteriores. La comprobación
+ estructural y de límites sí cubre la definición completa previamente.
+- Selected conserva el ID de la regla elegida; lista vacía identifica el default.
+ La persistencia de esa evidencia en auditoría pertenece a las tareas posteriores.
+
+## JSON Pointer y contexto
+
+El resolver solo admite las raíces tipadas /request/data, /event/data, /results y
+/actor. Desescapa ~1 y ~0 una sola vez y admite claves vacías y caracteres Unicode.
+En arrays acepta índices decimales canónicos sin ceros iniciales; índices fuera de
+rango, '-' y acceso a un hijo de un escalar no resuelven valor. No ejecuta expresiones.
+
+Actor expone subject, businessScope y roles provenientes del snapshot confiable;
+roles se ordenan para que su representación sea determinista. Los campos del evento
+no sustituyen esa identidad. Resolver raíz /request/data devuelve únicamente data,
+no identidad, estado, revisión ni ACL de la solicitud.
+
+## Límites
+
+EvaluationLimits permite reducir límites hasta los techos operativos: profundidad
+20 y 1000 nodos. Se configura al construir el evaluador; el wiring del motor vendrá
+con los servicios que lo consumen. No es configuración editable del flujo.
+
+La raíz de cada condición tiene profundidad 1. Cada condición y cada expresión
+path/literal cuenta como nodo; una comparación tiene tres nodos y profundidad 2.
+Los literales son hojas del AST. Se suman todas las reglas y ocurrencias, aunque
+compartan el mismo objeto Java. Los IDs duplicados son error. El preflight usa una
+pila explícita y rechaza también ramas que un cortocircuito habría omitido.
+
+Para acotar comparaciones de datos, IN admite hasta maxNodes elementos y la
+igualdad estructural hasta maxNodes pares visitados por comparación. Exceder el
+presupuesto devuelve EVALUATION_ERROR. Esto no sustituye límites de bytes al
+recibir/deserializar: definición 1 MiB y datos 256 KiB siguen pendientes de T06/T09.
+
+## Solapamientos estáticos
+
+IntervalOverlapAnalyzer reconoce EQ/GT/GTE/LT/LTE numéricos de path frente a literal,
+operandos invertidos y conjunciones AND del mismo path. Intersecta extremos abiertos
+y cerrados con BigDecimal y descarta intervalos vacíos. Devuelve parejas de IDs y
+path con solapamiento demostrado, además de IDs de reglas fuera del subconjunto.
+
+No analiza OR/NOT/EXISTS/IN/NEQ ni compara intervalos sobre paths diferentes.
+La ausencia de solapamientos encontrados no certifica disjunción general. T06
+consumirá estos diagnósticos al publicar; FIRST puede tener precedencia intencional.
+UNIQUE mantiene obligatoriamente detección de ambigüedad en runtime.
+
+## Evidencia y alcance
+
+DecisionEvaluatorTest usa el flujo scoring.json contractual para 71/70/61/60 y
+fracciones de precisión alta. Las fixtures missing-score/string-score se rechazan
+por el schema BureauResult y, si llegan al evaluador, producen EVALUATION_ERROR,
+nunca una salida de scoring bajo. No es todavía una prueba de adapter de buró.
+
+Las suites incluyen los once operadores, null/ausencia, IN heterogéneo, condiciones
+anidadas, cortocircuito, FIRST/UNIQUE, límites exactos, 50000 niveles de entrada,
+uso concurrente, JSON Pointer y extremos de intervalos. La prueba de arquitectura
+incluye el evaluador de application y lo compila con únicamente java.base.
+
+No hay nuevos endpoints, llamadas externas, cambios SQL ni escritura de solicitudes.
+El mapeo JSON a DSL usado por la prueba scoring es exclusivamente un fixture de
+pruebas; el parser/validador completo de publicación corresponde a T06. Los criterios
+A04–A06 se verifican al nivel de decisiones/contrato, no como flujo REST completo.
diff --git a/specs/DOMAIN_MODEL.md b/specs/DOMAIN_MODEL.md
new file mode 100644
index 0000000..a5d7f06
--- /dev/null
+++ b/specs/DOMAIN_MODEL.md
@@ -0,0 +1,94 @@
+# Modelo de dominio y puertos (T04)
+
+El dominio utiliza Java 21 y no depende de Spring, serializadores, HTTP, JDBC ni
+JPA. Sus records son snapshots inmutables, no entidades administradas ni cuerpos
+REST. Las escrituras y los bloqueos pertenecen a servicios de aplicación futuros.
+
+| Paquete | Modelos y responsabilidades |
+|---|---|
+| shared/domain | Identidades UUID distintas por recurso, Actor confiable, JSON inmutable, ErrorCode y DomainException |
+| flow/domain | DSL FlowDefinition, estados/eventos, transiciones, rutas fijas o por decisión, tareas, schemas, FlowVersion y ValidationReport |
+| decision/domain | AST cerrado de condiciones/valores, reglas ordenadas, política explícita UNIQUE/FIRST, contexto y resultado |
+| action/domain | Configuración por capacidad, reintentos acotados, contexto, resultado y metadatos de catálogo confiable |
+| request/domain | RequestInstance fijada a FlowId y RequestType con activación opcional |
+| execution/domain | Estado técnico, ejecución aceptada, lease/fencing y checkpoint de acción |
+| */application/port | DecisionEvaluator, ActionHandler y FlowDefinitionValidator |
+| flow/api | ValidationReportResponse y conversión explícita desde dominio al contrato REST |
+
+## Invariantes y representación
+
+- Los estados y eventos de negocio son claves configurables, no enums Java por
+ proceso. Los enums representan únicamente conceptos técnicos cerrados.
+- FlowDefinition representa el DSL; FlowVersion agrega identidad, estado de
+ publicación, revisión, checksum y fechas. RequestInstance conserva solo FlowId,
+ sin duplicar la versión. RETIRED es ejecutable para instancias existentes y no
+ asignable a nuevas solicitudes.
+- List.copyOf y Map.copyOf/Set.copyOf impiden cambios por alias. Los hijos JSON
+ también son inmutables: objetos, listas, strings, BigDecimal, boolean y null
+ explícito. Lookup.Missing no es un valor JSON; no se confunde con NullValue.
+- Los números se construyen con BigDecimal, sin coerción implícita desde strings.
+ La comparación numérica, resolución de pointers y límites de evaluación son T05.
+- Condiciones, acciones y rutas son variantes selladas. EXISTS recibe un path;
+ NOT un operando; AND/OR conservan orden y requieren al menos dos argumentos.
+ Una transición tiene ruta fija o decisión/destinos, nunca ambas.
+- Los paths de lectura se restringen a request.data, event.data, results y actor.
+ Updates.target es relativo a request.data; /id significa data.id y no request.id.
+ La detección de destinos solapados y la validación de datos corresponden a T06.
+- Actor solo se construirá desde identidad verificada. No incluye JWT ni secretos
+ y no se toma del evento. EvaluationContext no expone servicios externos.
+- Optional representa campos ausentes del modelo interno; los adapters deben
+ omitirlos o traducirlos según el contrato, no serializar el dominio directamente.
+ Versiones de contrato/perfil y schemas pueden faltar en borradores; T06 debe
+ resolverlos y fijarlos al publicar. SchemaVersion desconocida se diagnostica
+ en validación, no se interpreta como otra versión del proceso.
+- ActionCapability procede del catálogo confiable. Su clasificación de efectos
+ y schemas no forman parte de ActionDefinition editable. NO_OP solo es para
+ simulación; T06 debe impedir publicarlo y T11 controlar su ejecución.
+- ActionResult.Completed admite resultados negativos normalizados del negocio.
+ TechnicalFailure expresa fallo técnico TRANSIENT, PERMANENT o UNKNOWN_EFFECT.
+ La elegibilidad de reintento también requiere revisar efectos y checkpoints;
+ el record no autoriza por sí solo un reintento.
+- ExecutionStatus.RECONCILIATION_REQUIRED sigue activo; SUCCEEDED/FAILED liberan
+ la exclusión. RUNNING exige lease y otros estados no lo admiten. SUCCEEDED
+ exige resultado, aunque el resultado conserve el estado anterior del negocio.
+- Los hashes usan 64 caracteres hexadecimales en minúsculas, como SQL y API.
+ Los contadores no son negativos y los checkpoints exitosos exigen resultado y
+ fecha final. El worker debe verificar tokens y tiempos de PostgreSQL en T10.
+
+## Errores y adaptadores
+
+ErrorCode mantiene códigos de máquina independientes de HTTP. DomainException
+expone solo el código, sin incorporar entradas ni respuestas crudas del proveedor.
+Los adaptadores API resolverán status, correlationId y mensajes permitidos.
+DecisionResult.Failure solo acepta EVALUATION_ERROR o AMBIGUOUS_DECISION; un
+fallo nunca se representa mediante la ruta default.
+
+ValidationReport contiene incidencias con JSON Pointer, código y severidad.
+ValidationReportResponse convierte solo errores al arreglo errors del contrato;
+las advertencias internas no invalidan el flujo ni agregan campos no publicados.
+El DTO se comprueba contra api.schema.json. No hay endpoint nuevo en T04.
+
+No hay anotaciones JPA ni entidades persistentes disfrazadas de records de dominio.
+Los futuros adapters de persistencia deberán mapear explícitamente sus entidades
+hacia estos snapshots; tampoco deben devolver entidades por REST.
+
+## Alcance verificado
+
+DomainModelTest cubre inmutabilidad, AST, pointers, ausencia/null, clasificación de
+resultados, versiones, leases/checkpoints, reintentos y errores. DomainBoundaryTest
+comprueba el DTO contra el esquema real. DomainArchitectureTest recompila todos
+los archivos domain y application/port con classpath vacío y solo java.base;
+además prohíbe referencias java.net/java.sql. Así detecta dependencias accidentales
+de infraestructura, API, Spring/JPA o clientes HTTP.
+
+No hay implementaciones ficticias de los puertos ni llamadas a proveedores.
+T05 implementará el evaluador; T06 el validador/publicación; T11 los handlers.
+Los constructors garantizan integridad local del objeto; no sustituyen validación
+del DSL, autorización, transacciones, canonicalización ni A01–A20.
+
+## Evaluación implementada en T05
+
+PureDecisionEvaluator implementa el puerto de decisiones. ConditionEvaluator,
+JsonPointerResolver, AstLimits y IntervalOverlapAnalyzer son componentes puros.
+Ver [semántica y límites](DECISION_EVALUATOR.md). T06/T11 conservan la implementación
+del validador y handlers; no se añadió ninguna llamada HTTP ni persistencia.
diff --git a/specs/EVENTS.md b/specs/EVENTS.md
new file mode 100644
index 0000000..584298d
--- /dev/null
+++ b/specs/EVENTS.md
@@ -0,0 +1,86 @@
+# Aceptación durable de eventos — T09
+
+Implementado `POST /api/v1/requests/{id}/events`, habilitado por
+`REQUEST_API_ENABLED=true` junto con T08. Requiere JWT válido, propietario y ámbito
+coincidentes, `Idempotency-Key` y un `If-Match` fuerte como `"0"`.
+
+```json
+{"event":"SUBMIT","data":{}}
+```
+
+El evento y sus datos deben corresponder al flujo publicado de la solicitud.
+No se aceptan estado, roles, identidad, resultados o transición desde el cliente.
+Se aplican los mismos límites de T08: datos canónicos de 256 KiB, cuerpo acotado
+a 256 KiB + 4096 bytes y profundidad JSON 64; rechazo de duplicados y UTF-8 inválido.
+
+## Protocolo
+
+1. Verificar acceso; bloquear la solicitud con SELECT FOR UPDATE, filtrando otra
+ vez propietario/ámbito dentro de la transacción.
+2. Buscar la clave por request_id/idempotency_key. Mismo hash y sujeto autorizado:
+ devolver executionId y estado técnico actual. Hash distinto: 409
+ IDEMPOTENCY_CONFLICT. El hash SHA-256 incluye evento, datos canónicos y revisión
+ esperada; cambiar If-Match también cambia el comando.
+3. Sin replay, comprobar revisión (412) y ausencia de ejecución activa (409
+ REQUEST_BUSY). La revisión se comprueba antes de busy, por lo que una carrera
+ con dos claves y la misma revisión produce un aceptado y otro 412.
+4. Leer la definición inmutable fijada por flow_id, sin resolver el activo del tipo.
+ PUBLISHED y RETIRED pueden ejecutar instancias existentes. Resolver la transición
+ para estado/evento, exigir uno de allowedRoles (403), validar inputSchema (422)
+ y evaluar guardia pura con request.data/event.data/actor confiables.
+5. Insertar execution PENDING con identidad mínima, evento, transición y estado
+ origen. Incrementar revisión y updated_at de solicitud, insertar EVENT_ACCEPTED.
+ Confirmar las tres escrituras en una sola transacción READ COMMITTED.
+6. Retornar 202 con Location `/api/v1/executions/{id}` y AcceptedEvent contractual.
+
+accepted_revision guarda la revisión **posterior a aceptar** (If-Match + 1), que
+T10 deberá verificar al confirmar trabajo. La solicitud conserva datos y estado
+de negocio; T09 no ejecuta acciones, transiciones finales ni HTTP a proveedores.
+No se guarda el JWT: actor_snapshot contiene subject, businessScope y roles.
+La auditoría registra transición, revisión y correlationId, sin copiar datos del evento.
+
+## Reintentos y errores
+
+El replay se comprueba antes de revisión, busy, transición, roles y guardia actuales.
+Devuelve el mismo executionId y status actual, incluso después de avanzar la
+solicitud o terminar la ejecución. Exige siempre acceso actual y el sujeto original;
+una revocación del rol de transición no vuelve a autorizar efectos, pues el replay
+solo consulta la aceptación anterior. No incrementa revisión ni duplica auditoría.
+Para reintentar un comando perdido, conservar también el If-Match original.
+
+Un evento nuevo usa otra clave. Mientras haya PENDING, RUNNING, RETRY_WAIT o
+RECONCILIATION_REQUIRED no se admite otra ejecución. Las restricciones SQL existentes
+son defensa adicional. Un error de persistencia revierte ejecución, revisión y
+auditoría; no consume la clave.
+
+Evento no válido en el estado: 409 INVALID_EVENT. Guardia falsa: 422
+GUARD_NOT_SATISFIED; ruta ausente/tipo incompatible: 422 EVALUATION_ERROR, no una
+condición falsa ni resultado de negocio. Sin If-Match: 428; débil/múltiple/inválido:
+400. Recurso ajeno/inexistente: 404; sin autenticación: 401; infraestructura: 503
+sin exponer SQL. Revisión agotada en Long.MAX_VALUE no se incrementa y rechaza 409.
+
+## Alcance y verificación
+
+El worker T10 y las integraciones T11 permanecen pendientes. Sin worker, los eventos
+aceptados permanecen PENDING. GET /requests/{id} muestra la revisión nueva y la
+ejecución activa. La URI Location es la contractual; el endpoint GET /executions/{id}
+todavía no está implementado y se completará con el seguimiento de ejecución T10.
+No se afirma idempotencia de efectos externos hasta implementar sus protocolos.
+
+EventAcceptanceIT usa PostgreSQL real, login restringido, JWT RSA/JWKS local y HTTP
+real. Reutiliza únicamente el fixture de T07/T08 en orquestador_test. Cubre A07–A09
+en aceptación: replay/409, carreras simultáneas, schemas y guardias, acceso/roles,
+flujo retirado fijado, estados activos y rollback inducido al insertar ejecución,
+actualizar revisión e insertar auditoría. Los cambios a SUCCEEDED/RUNNING/etc. son
+fixtures SQL explícitos de prueba; no simulan un worker terminado en producción.
+Una instancia nueva de adapter recupera el replay desde DB; no se anuncia una prueba
+de reinicio del proceso ni procesamiento externo. T16 verificará el recorrido completo.
+
+Comando: `mvnw.cmd clean verify -Pmigration-tests-external` con variables
+MIGRATION_TEST_DB_URL/USER/PASSWORD en entorno. Los esquemas/roles temporales propios
+se eliminan al terminar. No se modifica orquestador ni se despliega el servicio.
+
+Desde T10 GET /executions/{id} y worker opcional ya están implementados. La sección
+anterior de alcance corresponde al cierre de T09. Ver WORKER.md: sin WORKER_ENABLED
+los aceptados siguen PENDING; con worker habilitado los flujos sin acciones pueden
+concluir y las integraciones no registradas fallan explícitamente, sin mocks.
diff --git a/specs/FLOW_PUBLICATION.md b/specs/FLOW_PUBLICATION.md
new file mode 100644
index 0000000..2569902
--- /dev/null
+++ b/specs/FLOW_PUBLICATION.md
@@ -0,0 +1,132 @@
+# Validación y administración de flujos (T06)
+
+T06 entrega servicios internos verificables: FlowAdministration, FlowPreparation,
+ConfiguredFlowValidator, FlowJsonCodec y PostgresFlowStore. Todavía no hay
+controladores administrativos ni beans de catálogo predeterminados: T07 conectará
+la identidad JWT verificada y la autorización antes de exponer REST. No se ofrece
+un catálogo simulado en producción. Las firmas del servicio reciben Actor confiable,
+revisión esperada y correlationId; nunca deben mapearse esos atributos desde el body.
+
+## Operaciones
+
+| Operación interna | Rol técnico | Efecto atómico |
+|---|---|---|
+| create | FLOW_EDITOR | Borrador y auditoría CREATE |
+| edit | FLOW_EDITOR | Solo DRAFT, revisión esperada, identidad fija y auditoría EDIT |
+| validate | FLOW_EDITOR o FLOW_PUBLISHER | Reporte de errores por JSON Pointer |
+| publish | FLOW_PUBLISHER | Validar, fijar contratos/perfiles, checksum, PUBLISHED y auditoría |
+| activate | FLOW_ADMIN | Flujo PUBLISHED, tipo habilitado/existente, revisión del tipo y auditoría |
+| retire | FLOW_ADMIN | PUBLISHED a RETIRED, desactivar referencias activas y auditar |
+
+Los tipos se provisionan por migración/configuración administrativa, como establece
+el contrato T02. Al retirar, cada tipo que aún apunta a esa versión queda sin flujo
+activo e incrementa revision; su clave queda en admin_audit.detail.deactivatedTypeKeys.
+La operación no cambia ninguna instancia existente ni desactiva tipos que ya apuntan
+a otra versión. Toda escritura se revierte si falla insertar la auditoría.
+
+REVISION_CONFLICT es el código interno de revisión; el futuro adaptador REST lo
+traducirá al REVISION_MISMATCH contractual (412). INVALID_FLOW, RESOURCE_NOT_FOUND,
+ACCESS_DENIED y FlowValidationException tienen mensajes seguros. El adaptador debe
+traducirlos al contrato y no devolver causas SQL. No reutilizar entidades/snapshots
+como DTO REST. A16 tiene evidencia en servicio y DB; la comprobación HTTP con JWT
+real se completa al exponer las rutas en T07/T16.
+
+## Catálogo confiable y publicación
+
+PublicationCatalog es un snapshot inmutable aportado por configuración confiable:
+roles de negocio, handlers y sus schemas, integraciones por clave/versiones,
+versiones de perfiles y políticas documentales (MIME/tamaño). No contiene endpoints,
+headers ni secretos; no consulta disponibilidad de proveedores durante publicación.
+
+Versiones explícitas deben existir. Versiones omitidas en borrador se resuelven a
+los defaults del catálogo y se guardan dentro de la definición publicada antes del
+checksum. Rotar el default no cambia versiones ya guardadas. El despliegue deberá
+conservar los contratos/perfiles requeridos por versiones en ejecución. Las fixtures
+de prueba describen contratos de ejemplo; no son adapters ni respuestas de un buró.
+
+Se validan:
+
+- Estructura DSL, schemaVersion, límites, claves duplicadas y schemas embebidos.
+- Identificadores únicos, estado inicial, destinos, alcanzabilidad, terminales,
+ estados sin salida y una transición por pareja estado/evento.
+- Roles conocidos, handlers/contratos/perfiles resolubles y prohibición de NO_OP.
+- Inputs obligatorios/permitidos, referencias a resultados anteriores, tipos de
+ mappings y literales contra su schema. No se permiten referencias adelantadas.
+- Cobertura exacta de salidas de decisiones, límites del AST, tipos básicos y
+ solapamientos UNIQUE demostrables mediante intervalos simples. FIRST conserva
+ precedencia explícita; el evaluador conserva controles de runtime.
+- Updates relativos a data, destinos existentes y no solapados. Las rutas escritas
+ desde results se consideran datos de proveedor: no pueden ser aceptadas en
+ initialDataSchema/eventos ni escritas luego desde un origen no confiable.
+- Tareas ligadas a transición/evento y equivalencia canónica de inputSchema.
+ DOCUMENT_UPLOAD requiere documentId UUID obligatorio, mapping desde el evento,
+ REGISTER_DOCUMENT del mismo tipo y límites dentro de la política confiable.
+
+Publicación exige dataSchema de objeto e inputs iniciales/de eventos con type=object
+expreso y additionalProperties=false. Si initialDataSchema falta se utiliza dataSchema;
+si este permite recibir campos de proveedor, la publicación se rechaza hasta añadir
+un schema inicial restrictivo. Los ejemplos estructurales de T02 no se convierten
+silenciosamente en flujos publicables ni sustituyen los recorridos completos T14.
+
+La compatibilidad estática comprueba tipos y literales, no implica demostrar inclusión
+arbitraria entre JSON Schemas ni resolver SAT. T08 debe validar datos iniciales contra
+ambos schemas, T11 salidas normalizadas y T10 datos candidatos antes del commit.
+La existencia de mappings válidos no autoriza a omitir esas validaciones en runtime.
+
+## JSON y checksum
+
+El decoder rechaza claves duplicadas, tokens adicionales, JSON de más de 1 MiB,
+nesting JSON mayor a 64 y números fuera del presupuesto. El límite de AST de T05
+se aplica adicionalmente. Se rechazan NUL y surrogates aislados que no pueden
+conservarse de forma segura en UTF-8/JSONB. Se usan los schemas originales empaquetados
+en el JAR, sin duplicarlos ni aceptar referencias externas suministradas por un flujo.
+
+El formato canónico de la aplicación ordena claves de objetos, conserva orden de
+arrays y normaliza decimales con BigDecimal sin ceros finales. Conjuntos del modelo
+(roles/MIME) se ordenan. Defaults de reintento quedan explícitos al guardar el modelo.
+SHA-256 se calcula sobre UTF-8 de esa representación. No se afirma conformidad JCS
+(RFC 8785), cuyo modelo numérico es diferente. Cambiar esta canonicalización en el
+futuro requiere versionar/migrar con cuidado, no recalcular versiones publicadas.
+
+## Transacciones y orden de bloqueos
+
+PostgresFlowStore usa JDBC y TransactionTemplate con READ COMMITTED. El puerto y
+FlowAdministration siguen siendo Java puro. El motivo de esta decisión frente al
+JPA propuesto para agregados está registrado en ADR-010; no se introduce otro stack.
+
+1. Edición/publicación bloquean flow_definition y verifican revision/estado.
+2. Activación bloquea primero el flujo destino y después request_type; vuelve a
+ verificar revisión/habilitación antes de escribir.
+3. Retiro bloquea el flujo y los tipos referenciados en orden de clave. El UPDATE
+ solo borra referencias que todavía apuntan a la versión retirada.
+4. Para asignar una nueva instancia, assignableFlow lee el candidato sin bloqueo,
+ bloquea el flujo, bloquea el tipo y vuelve a verificar la referencia, enabled y
+ PUBLISHED. Un cambio concurrente produce conflicto, nunca una asignación ciega.
+
+T08 deberá invocar assignableFlow e insertar solicitud, idempotencia y auditoría
+**dentro de la misma transacción y DataSource**. No obtener el FlowVersion, cerrar la
+transacción y usarlo después para crear la solicitud. La sesión rechaza uso después
+de su cierre. Si la creación gana el bloqueo, su instancia queda fijada y el retiro
+posterior la respeta; si gana el retiro, no se permite la nueva asignación.
+
+No hay caché de activo. La resolución consulta PostgreSQL. No se modificaron V1/V2:
+los triggers y permisos existentes continúan protegiendo versiones e historial.
+Los administradores de DB conservan sus facultades; el servicio debe usar el rol
+limitado documentado en db/migration/README.md.
+
+## Ejecutar verificaciones
+
+Con JDK 21, `./mvnw test` comprueba contratos, modelo, decisiones y validación T06.
+`./mvnw clean verify` añade las dos suites PostgreSQL con Testcontainers. Sin Docker,
+usar migration-tests-external y las variables MIGRATION_TEST_DB_* descritas en RUNBOOK.
+
+FlowPublicationIT solo opera sobre orquestador_test en un esquema t06_ propio,
+que elimina al finalizar. Incluye fallos reales de auditoría mediante un trigger
+exclusivo del esquema de pruebas, barreras de concurrencia y ambos órdenes de la
+carrera asignación/retiro. No ejecuta migraciones sobre orquestador ni despliega el JAR.
+
+networknt 1.5.9 pasa de dependencia de test a runtime para validar publicación.
+Jackson 2 queda confinado en ese adaptador; los records de dominio no dependen de él.
+La configuración HTTP de Spring Boot continúa usando sus componentes existentes.
+La prueba de arquitectura recompila también la administración de aplicación con
+solo java.base, sin JDBC, Spring, serializadores ni API.
diff --git a/specs/IMPLEMENTATION_PLAN.md b/specs/IMPLEMENTATION_PLAN.md
index 76e9814..ce619b6 100644
--- a/specs/IMPLEMENTATION_PLAN.md
+++ b/specs/IMPLEMENTATION_PLAN.md
@@ -57,155 +57,192 @@ Dependencias: T02. Entregables: db/migration/, pruebas *IT.
Entorno objetivo actualizado por el usuario: PostgreSQL 9.6.2 disponible en server02; ver ADR-006. Usar DDL compatible con 9.6 y un entorno aislado para pruebas de escritura. La comprobación de conexión/arranque E11 no cierra esta tarea.
-- [ ] Crear tablas, FKs e índices.
-- [ ] Añadir idempotencia de creación/admin_audit/reconciliation_resolution.
-- [ ] Proteger inmutabilidad e historial con roles y triggers.
-- [ ] Probar migraciones desde cero.
+- [x] Crear tablas, FKs e índices.
+- [x] Añadir idempotencia de creación/admin_audit/reconciliation_resolution.
+- [x] Proteger inmutabilidad e historial con roles y triggers.
+- [x] Probar migraciones desde cero.
Verificación: Testcontainers PostgreSQL, rollback y migraciones. Cierre: Restricciones e inmutabilidad verificadas en PostgreSQL real.
+Cerrado el 2026-10-26: 92 contratos y 40 pruebas de integración aprobadas mediante clean verify -Pmigration-tests-external sobre PostgreSQL 9.6.2 real en orquestador_test. Migraciones, rollback, permisos y concurrencia verificados; aislamiento y alternativa a Docker local documentados en ADR-007/E12.
+
## T04 — Modelo de dominio y puertos
Dependencias: T02,T03. Entregables: flow/, request/, execution/, decision/, action/.
-- [ ] Definir records y errores estables.
-- [ ] Separar DTOs y entidades.
-- [ ] Implementar puertos DecisionEvaluator, ActionHandler y FlowDefinitionValidator.
+- [x] Definir records y errores estables.
+- [x] Separar DTOs y entidades.
+- [x] Implementar puertos DecisionEvaluator, ActionHandler y FlowDefinitionValidator.
Verificación: Pruebas de dominio y dependencias entre capas. Cierre: Dominio sin HTTP/JPA y contratos tipados.
+Cerrado el 2026-10-26: mvnw test con Java 21; 134 pruebas aprobadas, incluidas 42 de dominio/arquitectura/frontera. Dominio y puertos recompilados sin infraestructura y DTO validado contra el contrato existente. No hay entidades JPA ni endpoints nuevos; implementaciones de puertos pendientes de T05/T06/T11. Evidencia E13 y ADR-008.
+
## T05 — Evaluador de decisiones
Dependencias: T04. Entregables: decision/, pruebas de AST.
-- [ ] Implementar operadores/JSON Pointer/BigDecimal.
-- [ ] Distinguir null y ausente.
-- [ ] Aplicar límites y cortocircuito.
-- [ ] Resolver UNIQUE/FIRST y ambigüedad.
+- [x] Implementar operadores/JSON Pointer/BigDecimal.
+- [x] Distinguir null y ausente.
+- [x] Aplicar límites y cortocircuito.
+- [x] Resolver UNIQUE/FIRST y ambigüedad.
Verificación: JUnit parametrizado: 71/70/61/60, tipos y solapamientos. Cierre: A04–A06 y errores de evaluación comprobados.
+Cerrado el 2026-10-26: mvnw test con Java 21; 216 pruebas aprobadas, incluidas 82 nuevas de decisiones/pointers/intervalos. A04–A06 verificados al nivel de evaluador puro y contrato de salida; no como flujo REST completo. Límites, cortocircuito y arquitectura comprobados. Ver E14, ADR-009 y DECISION_EVALUATOR.md.
+
## T06 — Validador, publicación y activación
Dependencias: T03,T04,T05. Entregables: flow/, admin_audit, pruebas *IT.
-- [ ] Validar grafo, mappings, roles y capacidades.
-- [ ] Editar borradores con revision.
-- [ ] Publicar checksum canónico e inmutabilidad.
-- [ ] Activar/retirar con bloqueos y auditoría.
+- [x] Validar grafo, mappings, roles y capacidades.
+- [x] Editar borradores con revision.
+- [x] Publicar checksum canónico e inmutabilidad.
+- [x] Activar/retirar con bloqueos y auditoría.
Verificación: Pruebas PostgreSQL de carreras publicación/activación/retiro. Cierre: A16–A17; catálogo resoluble y publicación consistente.
+Cerrado el 2026-10-26 para servicios internos: clean verify -Pmigration-tests-external, 258 pruebas unitarias + 51 IT PostgreSQL aprobadas (309 total). A16 verificado en servicio/DB y A17 con asignación transaccional real; exposición REST/JWT queda en T07 conforme a ADR-010. No se anuncia A16 HTTP completo ni integración de proveedor real. Evidencia E15 y FLOW_PUBLICATION.md.
+
## T07 — Seguridad y autorización por recurso
Dependencias: T04. Entregables: shared/infrastructure/security/, autorización por capacidad.
-- [ ] Validar JWT real (firma/issuer/audience/exp).
-- [ ] Mapear permisos técnicos y roles.
-- [ ] Comprobar propiedad/scope en cada recurso.
-- [ ] Separar rol DB de ejecución.
+- [x] Validar JWT real (firma/issuer/audience/exp).
+- [x] Mapear permisos técnicos y roles.
+- [x] Conectar catálogo confiable/servicios T06 y exponer administración REST con pruebas JWT; completar A16 HTTP (ADR-010).
+- [x] Comprobar propiedad/scope en cada recurso.
+- [x] Separar rol DB de ejecución.
Verificación: Tokens firmados válidos/expirados, otro issuer/audience, scope/rol ajenos. Cierre: A15; rutas protegidas antes de exposición.
+Cerrado el 2026-10-27: 265 unitarias + 57 IT (322 total), sin fallos/errores/omisiones. Evidencia E16, SECURITY.md y ADR-011. A15 verificado mediante puerta real de recursos y controlador exclusivo de test; endpoints de negocio futuros deben integrarla. Keycloak real y despliegue pendientes. Simulación HTTP pendiente; seguimiento explícito en T16 (ADR-018).
+
## T08 — Crear y consultar solicitudes
Dependencias: T06,T07. Entregables: request/, repositorios y API.
-- [ ] Resolver y fijar flujo activo.
-- [ ] Validar datos iniciales.
-- [ ] Idempotencia durable de creación.
-- [ ] Exponer consulta y ETag con acceso por recurso.
+- [x] Resolver y fijar flujo activo.
+- [x] Validar datos iniciales.
+- [x] Idempotencia durable de creación.
+- [x] Exponer consulta y ETag con acceso por recurso.
Verificación: IT creación repetida/conflictiva y activación concurrente. Cierre: Creación atómica e idempotente, versión fijada.
+Cerrado el 2026-10-27: clean verify -Pmigration-tests-external, 268 unitarias + 66 IT (334 total), cero fallos/errores/omisiones. Creación/replay/consulta HTTP, activación y retiro con barreras, rollback SQL. Evidencia E17, REQUESTS.md y ADR-012. Sin despliegue.
+
## T09 — Aceptar eventos idempotentes
Dependencias: T08. Entregables: execution/api, aceptación, auditoría.
-- [ ] Bloquear solicitud.
-- [ ] Verificar hash antes de revisión.
-- [ ] Validar If-Match/rol/guardia.
-- [ ] Insertar PENDING, revision y auditoría atómicamente.
-- [ ] Responder 202/Location.
+- [x] Bloquear solicitud.
+- [x] Verificar hash antes de revisión.
+- [x] Validar If-Match/rol/guardia.
+- [x] Insertar PENDING, revision y auditoría atómicamente.
+- [x] Responder 202/Location.
Verificación: IT duplicados, conflictos, concurrencia y rollback. Cierre: A07–A09; aceptación durable sin HTTP.
+Cerrado el 2026-10-27: clean verify -Pmigration-tests-external, 268 unitarias + 76 IT (344 total), cero fallos/errores/omisiones. A07–A09 cubiertos en aceptación, sin acciones externas. Evidencia E18, EVENTS.md y ADR-013. Worker y GET ejecución pendientes T10; sin despliegue.
+
## T10 — Worker PostgreSQL y leases
Dependencias: T09. Entregables: execution/infrastructure, worker y repositorios.
-- [ ] Reclamar con SKIP LOCKED.
-- [ ] Renovar lease con tiempo DB.
-- [ ] Exigir owner/token en escrituras.
-- [ ] Checkpoints por acción y recuperación inicial.
+- [x] Reclamar con SKIP LOCKED.
+- [x] Renovar lease con tiempo DB.
+- [x] Exigir owner/token en escrituras.
+- [x] Checkpoints por acción y recuperación inicial.
+- [x] Exponer GET /executions/{id} con autorización y estado técnico (seguimiento del Location T09; ADR-013).
Verificación: IT dos workers, lease vencido y checkpoint; no @Async en memoria. Cierre: Cola durable multiinstancia; worker obsoleto no confirma.
+Cerrado el 2026-10-27. Suite completa: 268 unitarias + 91 IT; tras dos controles adicionales, verificación final: 268 unitarias + 17 WorkerIT. Evidencia vigente de 361 casos distintos (268 + 93), todos aprobados. Ver E19, WORKER.md y ADR-014. Sin despliegue ni proveedores reales; adapters T11 y política completa de retry T12 pendientes.
+
## T11 — Handlers e integraciones HTTP
Dependencias: T10. Entregables: action/, integration/, mocks, compose.yaml.
-- [ ] Implementar catálogo confiable y REGISTER_DOCUMENT/INVOKE_INTEGRATION/NO_OP sólo simulación.
-- [ ] Fijar contratos/perfiles.
-- [ ] Mapear entradas/salidas y timeouts.
-- [ ] Añadir mocks.
+- [x] Implementar catálogo confiable y REGISTER_DOCUMENT/INVOKE_INTEGRATION/NO_OP sólo simulación.
+- [x] Fijar contratos/perfiles.
+- [x] Mapear entradas/salidas y timeouts.
+- [x] Añadir mocks.
-Verificación: Mocks de cliente encontrado/no encontrado, timeout y salida inválida. Cierre: A01–A03; fallos técnicos separados del resultado de negocio.
+Verificación: Mocks HTTP de cliente encontrado/no encontrado, timeout y salida
+inválida; worker con PostgreSQL real, contratos fijados y rollback documental.
+Cierre T11: A01/A02 y separación técnica de A03. La programación completa de
+reintentos de A03 corresponde a T12; ver ADR-015. Proveedores reales requieren
+contratos/credenciales confirmados y no son sustituidos por evidencia de mocks.
+
+Cerrado el 2026-10-27: 301 unitarias + 99 IT = 400 aprobadas en la suite completa, sin fallos/errores/omisiones. Evidencia E20, INTEGRATIONS.md y ADR-015. Mocks HTTP locales y PostgreSQL real; integración real pendiente de contratos/credenciales. Siguiente T12.
## T12 — Reintentos y conciliación
Dependencias: T11. Entregables: execution/, endpoint resolve, pruebas *IT.
-- [ ] Limitar intentos/backoff/jitter.
-- [ ] Recuperar leases según clasificación de efecto.
-- [ ] Tratar UNKNOWN.
-- [ ] Conciliar con evidencia y autorización/idempotencia.
+- [x] Limitar intentos/backoff/jitter.
+- [x] Recuperar leases según clasificación de efecto.
+- [x] Tratar UNKNOWN.
+- [x] Conciliar con evidencia y autorización/idempotencia.
Verificación: Fault injection antes/después de envío, checkpoint y commit. Cierre: A12–A14,A19; efectos inciertos no liberan solicitud.
+Cerrado el 2026-10-27: 303 unitarias + 116 IT = 419 aprobadas; 17 casos T12 incluyen cinco caídas reales de JVM, retries, conciliación concurrente y rollback. Ver E21, RECONCILIATION.md y ADR-016. Sin despliegue ni proveedor real. Siguiente T13.
+
## T13 — Documentos y tareas para front
Dependencias: T11,T12. Entregables: document/, request/available-events.
-- [ ] Verificar pertenencia/tipo/MIME/tamaño/checksum/antimalware.
-- [ ] Guardar referencias.
-- [ ] Exponer schemas/UI y bloqueos.
-- [ ] Impedir escritura de resultados del proveedor.
+- [x] Verificar pertenencia/tipo/MIME/tamaño/checksum/antimalware.
+- [x] Guardar referencias.
+- [x] Exponer schemas/UI y bloqueos.
+- [x] Impedir escritura de resultados del proveedor.
Verificación: Documento ajeno/inválido, ejecución activa y payload con score. Cierre: A11,A20; INVALID conserva tarea, error técnico no equivale a INVALID.
+Cerrado el 2026-10-27: 308 unitarias + 137 IT = 445 aprobadas; incluye 8 casos de disponibilidad y 13 documentales con PostgreSQL real y mocks HTTP. Ver E22, AVAILABLE_EVENTS.md y ADR-017. Backend únicamente; sin interfaz gráfica, despliegue ni documentos reales. Siguiente T14.
+
## T14 — Flujos demostrativos v1/v2
Dependencias: T05,T06,T13. Entregables: examples/flows/, examples/http/.
-- [ ] Crear crédito v1, crédito v2 con licencia y compra v1 completos.
-- [ ] Publicar/activar y recorrer.
-- [ ] Demostrar coexistencia de versiones sin cambios Java/columnas.
+- [x] Crear crédito v1, crédito v2 con licencia y compra v1 completos.
+- [x] Publicar/activar y recorrer.
+- [x] Demostrar coexistencia de versiones sin cambios Java/columnas.
Verificación: Recorridos completos de v1/v2, licencia válida/inválida y fronteras. Cierre: A10,A18; dos tipos de solicitud publicables.
+Cerrado el 2026-10-28: 311 unitarias + 29 IT = 340 aprobadas; demo 10, documentos 13 y HTTP 6. Ver E23 y ADR-018. Tres flujos publicados/activados y recorridos con mocks; src/main y JAR idénticos a T13. Siguiente T15.
+
## T15 — Auditoría y observabilidad
Dependencias: T09,T10,T11,T12,T13,T14. Entregables: audit/, métricas, logs, health.
-- [ ] Completar consultas paginadas/filtradas.
-- [ ] Métricas sin cardinalidad por solicitud.
-- [ ] Logs sin PII.
-- [ ] Readiness/liveness y apagado ordenado.
+- [x] Completar consultas paginadas/filtradas.
+- [x] Métricas sin cardinalidad por solicitud.
+- [x] Logs sin PII.
+- [x] Readiness/liveness y apagado ordenado.
Verificación: Rollback/auditoría, acceso a historial, métricas y apagado. Cierre: Auditoría atómica desde T06 y operación observable.
+Cerrado el 2026-10-28: 314 unitarias + 163 IT = 477 aprobadas, más 4 unitarias + 1 IT para el ajuste final de cursor. Ver E24, OBSERVABILITY.md y ADR-019. Historial/ACL, métricas, logs, DB-health, apagado y rollback verificados localmente; siguiente T16.
+
## T16 — Verificación integral
Dependencias: T15. Entregables: src/test/, CI, matriz A01–A20.
-- [ ] Automatizar A01–A20.
-- [ ] Reinicios y concurrencia deterministas.
-- [ ] Verificar fronteras de rollback y seguridad.
-- [ ] Conservar reportes.
+- [x] Automatizar A01–A20.
+- [x] Reinicios y concurrencia deterministas.
+- [x] Verificar fronteras de rollback y seguridad.
+- [x] Conservar reportes.
+- [x] Cerrar pendiente del endpoint puro /admin/flows/{id}/simulate, registrado desde T07 (ADR-018); implementado/verificado por SimulationIT y FlowSimulationTest (ADR-020).
+
+- [ ] Ejecutar suite completa y gate A01–A20 con Docker/Testcontainers operativo.
Verificación: ./mvnw clean verify con Docker, sin skips de integración. Cierre: Todos los casos obligatorios pasan en entorno reproducible.
+Estado 2026-10-28: BLOCKED solo por entorno Docker. Implementación/simulación, CI y matriz completos; 493 pruebas Java y 2 del checker aprobadas con PostgreSQL externo. Testcontainers confirmó ausencia de motor válido. Ver E25, ACCEPTANCE.md y ADR-020; no equivale a cierre Docker.
+
## T17 — Empaquetado y documentación
Dependencias: T16. Entregables: Dockerfile, compose.yaml, README, RUNBOOK, examples/http, CI.
diff --git a/specs/INTEGRATIONS.md b/specs/INTEGRATIONS.md
new file mode 100644
index 0000000..d7cf86b
--- /dev/null
+++ b/specs/INTEGRATIONS.md
@@ -0,0 +1,88 @@
+# Integraciones HTTP — T11
+
+El worker utiliza `RegisteredWorkerActions`: `INVOKE_INTEGRATION` invoca HTTP y
+`REGISTER_DOCUMENT` verifica metadatos y guarda una referencia bajo fencing.
+`NO_OP` existe exclusivamente como utilidad de simulación y no está registrado
+en Spring ni en el worker; la publicación continúa rechazándolo.
+
+## Configuración de despliegue
+
+Se necesitan dos archivos confiables, fuera de los eventos/definiciones editables:
+
+- `FLOW_CATALOG_PATH`: contratos JSON Schema, efectos, perfiles/versiones y políticas documentales.
+- `INTEGRATION_ROUTES_PATH`: endpoint y límites para cada combinación exacta de integración, contrato, perfil y versión.
+
+Los archivos por defecto en `config/` tienen registros vacíos: no habilitan llamadas.
+Los ejemplos de `examples/integrations/` describen un protocolo local propuesto,
+no contratos confirmados de proveedores. Sus dominios `.invalid` son marcadores.
+Copiar/adaptar ambos archivos, mantener todas las versiones usadas por solicitudes
+vigentes y configurar las rutas antes de habilitar `WORKER_ENABLED=true`.
+Cambiar un default no modifica las versiones fijadas al publicar. No sustituir
+el significado de una versión existente: crear otra versión/tupla.
+
+Compose conserva un único servicio y PostgreSQL externo. Monta `./config:/app/config:ro`;
+la ruta predeterminada dentro del contenedor es `/app/config/integration-routes.json`.
+Los tokens se referencian por nombre de variable con `tokenEnvironment`.
+Declarar explícitamente esa variable en el entorno del servicio Docker (un valor
+en el .env de Compose no se transmite automáticamente al contenedor). No guardar
+el token en los JSON, los flujos ni los eventos. Cadena vacía permite un endpoint
+sin bearer cuando el contrato de despliegue lo requiera.
+
+## Protocolo y límites
+
+- POST JSON con inputs resueltos por el worker, validados contra el contrato.
+- Headers: Idempotency-Key estable por ejecución/acción, X-Correlation-Id,
+ X-Integration-Profile y X-Profile-Version cuando corresponda; bearer opcional.
+- HTTPS; HTTP solo loopback con perfil Spring `local` o `test`. Sin redirecciones,
+ userinfo, query o fragment en la URL.
+- connectMs positivo; timeoutMs cubre headers y cuerpo completo, máximo 30 s y
+ menor que el lease. maxBytes limita memoria antes de superar 256 KiB.
+- Respuesta JSON objeto, UTF-8 estricto, sin claves duplicadas ni coerción de
+ strings a números. Decimales conservan precisión.
+- outputMappings mapea campos superiores de salida a JSON Pointers de la
+ respuesta. Vacío significa identidad. La salida normalizada debe cumplir schema.
+- Contratos inline: no se resuelven referencias remotas desde el worker.
+
+Un 2xx válido con `found=false` o `status=INVALID` es éxito técnico y resultado
+de negocio negativo. Un 404 es error salvo `notFoundOutput` explícito, permitido
+solo para contratos READ_ONLY; ese resultado también se valida. No habilitar esa
+traducción sin confirmar el contrato real.
+
+Timeout/429/500/502/503/504 son transitorios; con NON_IDEMPOTENT_WRITE se considera
+efecto incierto. Una salida inválida tras escritura también es incierta. Los
+códigos internos no exponen cuerpo remoto, URL o token. La clasificación no
+garantiza idempotencia del proveedor: solo declarar IDEMPOTENT_WRITE si existe
+garantía contractual para la clave enviada.
+
+**T12:** el worker programa RETRY_WAIT para fallos transitorios seguros, con
+backoff/jitter y Retry-After acotados, hasta el máximo publicado de tres intentos
+por acción. Conserva checkpoints; efectos inciertos requieren conciliación.
+Consultar RECONCILIATION.md para el endpoint y la semántica de A03.
+
+## Documentos
+
+REGISTER_DOCUMENT consulta el protocolo fijo `DOCUMENTS_METADATA` contrato `1`,
+sin perfil, con documentId/requestId/businessScope. El servicio documental debe
+responder identidad de documento/solicitud/ámbito, tipo, identificador de storage,
+MIME, tamaño entero, checksum SHA-256 hexadecimal y scanStatus=CLEAN. El handler
+compara pertenencia y política; no confía en metadatos enviados por el cliente.
+
+La inserción en document_reference y el checkpoint SUCCEEDED comparten transacción
+y fencing, con bloqueo de solicitud antes de ejecución. Una referencia existente
+solo se reutiliza si coincide completamente. Fallo SQL revierte ambas escrituras.
+No se descargan/suben archivos ni se ejecuta un antivirus: se verifica el dictamen
+del servicio documental confiable. T13 completa eventos disponibles y tareas de front.
+
+## Verificación reproducible
+
+`mvnw.cmd test` ejecuta mocks HTTP loopback sin PostgreSQL ni proveedores reales.
+`mvnw.cmd clean verify -Pmigration-tests-external` agrega PostgreSQL real utilizando
+MIGRATION_TEST_DB_URL/USER/PASSWORD en el proceso y base exclusiva de pruebas.
+Sin ese perfil, verify utiliza Testcontainers y requiere Docker.
+
+HttpIntegrationTest prueba transporte y contratos; RegisterDocumentTest verifica
+metadatos y carga de ejemplos; HttpIntegrationIT conecta publicación, API de
+solicitudes/eventos, worker, HTTP y PostgreSQL con rollback inducido.
+Los estados START/END del fixture representan los destinos de A01/A02; los flujos
+demostrativos con nombres funcionales corresponden a T14. No hay integración real
+aprobada de clientes, documentos, buró o Keycloak ni despliegue en server02:8787.
diff --git a/specs/OBSERVABILITY.md b/specs/OBSERVABILITY.md
new file mode 100644
index 0000000..7fd7884
--- /dev/null
+++ b/specs/OBSERVABILITY.md
@@ -0,0 +1,100 @@
+# Auditoría y operación (T15)
+
+## Historial
+
+`GET /api/v1/requests/{id}/history` requiere REQUEST_API_ENABLED y JWT válido.
+Aplica la misma propiedad y businessScope que GET de solicitud; FLOW_ADMIN y
+EXECUTION_OPERATOR no tienen un bypass para consultar historial ajeno. Un recurso
+ajeno/inexistente responde 404. La respuesta cumple HistoryPage y usa no-store.
+
+Filtros opcionales: kind, executionId, from inclusivo y to exclusivo (UTC terminado
+en Z). limit por defecto 50, rango 1–100. Orden ascendente por (createdAt,id),
+mediante keyset; cursor opaco de máximo 2000 caracteres ligado a solicitud y filtros.
+Se permite cambiar limit conservando los demás filtros. Al terminar se omite
+nextCursor. Cursor inválido o usado con otros filtros devuelve 400.
+
+El cursor representa posición, no autorización ni una credencial firmada. Cada
+página vuelve a autorizar el recurso; modificar una posición nunca amplía su acceso.
+Los inserts concurrentes pueden aparecer en páginas posteriores; no es una foto
+transaccional de toda la navegación. Reiniciar desde from permite releer períodos
+cuando hay transacciones que confirmaron tarde. El consumidor deduplica por id.
+
+Proyección de detail permitida: correlationId, transitionId, acceptedRevision,
+ruleIds, reasonCodes, actionIds, hitPolicy, code, actionId, delayMs, mode y resolutionId.
+Se omiten datos de entrada, resultados completos, tokens y evidencia del operador.
+actorSubject se entrega únicamente al propietario autorizado. Nuevas transiciones
+guardan reasonCodes de resultados de acciones cuando existen; no se reconstruyen
+razones para auditorías históricas que no las contenían.
+
+Se mantienen las escrituras atómicas T06–T12 y triggers append-only V2. No hay una
+segunda escritura de auditoría posterior al commit ni una migración nueva. El rol
+runtime no modifica ni elimina historial. Retención/eliminación requiere una política
+de datos aprobada y procedimiento privilegiado aparte; este servicio no borra
+auditoría automáticamente ni interpreta un TTL como permiso de borrado.
+
+## Métricas
+
+`/actuator/metrics` y `/actuator/metrics/{name}` requieren EXECUTION_OPERATOR y
+businessScope incluido en ADMIN_BUSINESS_SCOPES. No dependen de habilitar edición
+de flujos. Actuator restante está denegado salvo las tres rutas públicas de salud.
+No se agrega servidor Prometheus ni dependencia nueva; Micrometer/Actuator ya están
+incluidos en el servicio.
+
+| Nombre | Tipo/etiquetas | Significado |
+| --- | --- | --- |
+| orchestrator.commands | Counter: command=create/event; result=accepted/rejected/error | Respuestas HTTP de comandos, incluidos replays; accepted es 2xx, error 5xx y rejected resto |
+| orchestrator.executions | Gauge: state fijo | Filas durables por estado técnico, compartidas entre réplicas |
+| orchestrator.pending.oldest.seconds | Gauge sin etiquetas | Edad DB del PENDING más antiguo; cero sin pendientes |
+| orchestrator.leases.expired | Gauge sin etiquetas | RUNNING con lease vencido; no implica efecto externo duplicado |
+| orchestrator.queue.sample.available | Gauge sin etiquetas | 1 muestra SQL disponible; 0 fallo; demás gauges NaN al fallar |
+| orchestrator.integration.duration | Timer: integration, result=completed/failed/error | Tiempo del adapter HTTP, incluido contrato/mapeo; solo claves del catálogo/rutas permitidas |
+
+Los gauges se consultan bajo demanda y comparten una muestra SQL por proceso de
+5 segundos; query timeout 3 segundos. Son estados actuales, no contadores históricos
+de transiciones. RECONCILIATION_REQUIRED expone conciliaciones pendientes y RETRY_WAIT
+reintentos programados. No hay etiquetas por UUID, sujeto, ámbito, URL, perfil,
+excepción libre o datos de proveedor. Las claves de integración pertenecen a
+configuración confiable. Contadores/timers locales se reinician con el proceso;
+no son el ledger durable. Para varias réplicas no sumar gauges de la misma DB.
+
+## Logs y correlación
+
+El filtro genera X-Correlation-Id; no confía en uno suministrado por el cliente.
+Comandos registran tipo, resultado HTTP, código de estado, correlación y duración.
+Worker registra requestId, executionId, flowId, versión; acciones añaden actionId,
+intento, resultado técnico y duración. `phase=committed` se emite después del commit.
+Un log de acción completada puede preceder un rollback: consultar ejecución/historial
+para el resultado durable. No registra payload, documento, sujeto, token, score,
+evidencia ni excepción con body de proveedor. correlationId de EVENT_ACCEPTED enlaza
+HTTP con executionId del historial y del worker; el adapter propaga executionId
+como X-Correlation-Id estable entre intentos. Las decisiones y razones quedan en
+auditoría restringida, no en logs.
+
+## Salud y apagado
+
+`/actuator/health/liveness` consulta disponibilidad del proceso, sin DB/proveedores.
+`/actuator/health/readiness` añade PostgreSQL; falla 503 cuando DB no está disponible.
+`/actuator/health` es salud agregada. Las rutas públicas omiten detalles/componentes.
+No se llama a clientes/documentos/buró para determinar salud.
+
+Spring usa server.shutdown=graceful y 30 s por fase. El worker deja de reclamar,
+cierra el poller (hasta 2 s) y conserva heartbeats mientras drena trabajos durante
+WORKER_SHUTDOWN_SECONDS (1–20, predeterminado 20). Al agotar, interrumpe trabajos y
+espera 1 s adicional; cancela renovaciones. Los adapters deben respetar interrupción
+y sus límites de tiempo. Una operación no cooperativa puede seguir hasta su timeout;
+no se presume cancelación de un efecto externo. Cola/checkpoints permanecen durables
+y se recuperan por lease/fencing y conciliación T12. No se liberan leases por apagar.
+Compose mantiene stop_grace_period=40s para la JVM y PostgreSQL externo.
+
+## Verificación
+
+ObservabilityIT usa API/JWT real, esquema y rol temporales en orquestador_test:
+historial/ACL/filtros/cursor/empates, métricas SQL, Actuator y logs con sentinelas.
+Cierra únicamente su propio pool para demostrar readiness 503/liveness 200.
+ObservabilityTest usa latches y worker real con store controlado para drenaje,
+renovación e interrupción dentro del presupuesto; HTTP loopback prueba timers.
+WorkerIT conserva pruebas reales de rollback de auditoría, recuperación y fencing.
+No es una prueba de despliegue Docker ni de caída de PostgreSQL de producción.
+
+Fuentes de API: [Micrometer](https://docs.micrometer.io/micrometer/reference/concepts/meters.html)
+y [Actuator Health](https://docs.spring.io/spring-boot/api/rest/actuator/health.html).
diff --git a/specs/PROGRESS.md b/specs/PROGRESS.md
index a5bbe6b..c495240 100644
--- a/specs/PROGRESS.md
+++ b/specs/PROGRESS.md
@@ -1,6 +1,6 @@
# Seguimiento de implementación
-Actualizado: 2026-10-26. T01 y T02 DONE. Siguiente tarea: T03 (PostgreSQL/Flyway). El motor y endpoints de negocio no están implementados.
+Actualizado: 2026-10-28. T01–T15 DONE. T16 BLOCKED: implementación y A01–A20 aprobados con PostgreSQL externo; falta corrida Docker/Testcontainers. Ver E25. T17 PENDING; ninguna tarea principal IN_PROGRESS.
Estados permitidos: PENDING, IN_PROGRESS, BLOCKED, DONE. No más de una tarea principal IN_PROGRESS. Consultar IMPLEMENTATION_PLAN para subtareas y criterios.
@@ -8,20 +8,20 @@ Estados permitidos: PENDING, IN_PROGRESS, BLOCKED, DONE. No más de una tarea pr
|---|---|---|---|---|---|---|---|
| T01 | DONE | — | pom.xml, mvnw*, .mvn, configuración, DeploymentSmokeCheck.java, ADR-001/004 | E09: clean verify con JDK 21 y deployment-smoke | BUILD SUCCESS; 5 pruebas, 0 fallos, 0 errores, 0 omitidas | Ninguno para T01 | Iniciar T02: OpenAPI y JSON Schema |
| T02 | DONE | T01 DONE | contracts/openapi.yaml, schemas/*.schema.json, examples/contracts, ContractValidationTest.java, ADR-005 | E10: clean verify con Java 21 | BUILD SUCCESS; 92 comprobaciones, 0 fallos/errores/omisiones | Ninguno para T02 | Iniciar T03: migraciones PostgreSQL y Testcontainers |
-| T03 | PENDING | T02 DONE | db/migration/, pruebas *IT | E11 verifica conexión/arranque 9.6; migraciones pendientes | Compatibilidad inicial aprobada; modelo no implementado | Falta entorno aislado para pruebas de migraciones | Crear DDL compatible con PostgreSQL 9.6 según ADR-006 |
-| T04 | PENDING | T02,T03 | flow/, request/, execution/, decision/, action/ | Pendiente: Pruebas de dominio y dependencias entre capas | No ejecutado | Dependencias sin completar | Definir records y errores estables |
-| T05 | PENDING | T04 | decision/, pruebas de AST | Pendiente: JUnit parametrizado: 71/70/61/60, tipos y solapamientos | No ejecutado | Dependencias sin completar | Implementar operadores/JSON Pointer/BigDecimal |
-| T06 | PENDING | T03,T04,T05 | flow/, admin_audit, pruebas *IT | Pendiente: Pruebas PostgreSQL de carreras publicación/activación/retiro | No ejecutado | Dependencias sin completar | Validar grafo, mappings, roles y capacidades |
-| T07 | PENDING | T04 | shared/infrastructure/security/, autorización por capacidad | Pendiente: Tokens firmados válidos/expirados, otro issuer/audience, scope/rol ajenos | No ejecutado | Dependencias sin completar | Validar JWT real (firma/issuer/audience/exp) |
-| T08 | PENDING | T06,T07 | request/, repositorios y API | Pendiente: IT creación repetida/conflictiva y activación concurrente | No ejecutado | Dependencias sin completar | Resolver y fijar flujo activo |
-| T09 | PENDING | T08 | execution/api, aceptación, auditoría | Pendiente: IT duplicados, conflictos, concurrencia y rollback | No ejecutado | Dependencias sin completar | Bloquear solicitud |
-| T10 | PENDING | T09 | execution/infrastructure, worker y repositorios | Pendiente: IT dos workers, lease vencido y checkpoint; no @Async en memoria | No ejecutado | Dependencias sin completar | Reclamar con SKIP LOCKED |
-| T11 | PENDING | T10 | action/, integration/, mocks, compose.yaml | Pendiente: Mocks de cliente encontrado/no encontrado, timeout y salida inválida | No ejecutado | Dependencias sin completar | Implementar catálogo confiable y REGISTER_DOCUMENT/INVOKE_INTEGRATION/NO_OP sólo simulación |
-| T12 | PENDING | T11 | execution/, endpoint resolve, pruebas *IT | Pendiente: Fault injection antes/después de envío, checkpoint y commit | No ejecutado | Dependencias sin completar | Limitar intentos/backoff/jitter |
-| T13 | PENDING | T11,T12 | document/, request/available-events | Pendiente: Documento ajeno/inválido, ejecución activa y payload con score | No ejecutado | Dependencias sin completar | Verificar pertenencia/tipo/MIME/tamaño/checksum/antimalware |
-| T14 | PENDING | T05,T06,T13 | examples/flows/, examples/http/ | Pendiente: Recorridos completos de v1/v2, licencia válida/inválida y fronteras | No ejecutado | Dependencias sin completar | Crear crédito v1, crédito v2 con licencia y compra v1 completos |
-| T15 | PENDING | T09,T10,T11,T12,T13,T14 | audit/, métricas, logs, health | Pendiente: Rollback/auditoría, acceso a historial, métricas y apagado | No ejecutado | Dependencias sin completar | Completar consultas paginadas/filtradas |
-| T16 | PENDING | T15 | src/test/, CI, matriz A01–A20 | Pendiente: ./mvnw clean verify con Docker, sin skips de integración | No ejecutado | Dependencias sin completar | Automatizar A01–A20 |
+| T03 | DONE | T02 DONE | V1/V2 SQL, DatabaseMigrationIT.java, configuración, ADR-007, RUNBOOK | E12: clean verify -Pmigration-tests-external con JDK 21 | 92 contratos + 40 IT aprobados en PostgreSQL 9.6.2; 0 fallos/errores/omisiones | Ninguno para T03; ruta Docker no ejecutada localmente | T04: modelo de dominio y puertos |
+| T04 | DONE | T02,T03 DONE | */domain, */application/port, flow/api/ValidationReportResponse, Domain*Test, DOMAIN_MODEL.md, ADR-008 | E13: mvnw test con JDK 21; compilación aislada y DTO contra schema | 134 pruebas aprobadas; 0 fallos/errores/omisiones | Ninguno | T05: implementar evaluador de decisiones |
+| T05 | DONE | T04 DONE | decision/domain, PureDecisionEvaluator, tres suites T05, DECISION_EVALUATOR.md, ADR-009 | E14: mvnw test con JDK 21 | 216 pruebas aprobadas, 0 fallos/errores/omisiones; A04–A06 a nivel evaluador/contrato | Ninguno | T06: validador, publicación y activación |
+| T06 | DONE | T03,T04,T05 DONE | flow/application, flow/infrastructure, PublicationCatalog, FlowValidationTest, FlowPublicationIT, FLOW_PUBLICATION.md, ADR-010 | E15: clean verify -Pmigration-tests-external | 258 unitarias + 51 IT aprobadas; A16 servicio/DB y A17 asignación; REST pendiente T07 | Ninguno para servicios internos T06 | T07: JWT, autorización por recurso y exposición administrativa |
+| T07 | DONE | T04 DONE | shared/infrastructure/security, ResourceAccess, flow/api, SecurityPolicyTest, SecurityHttpIT, SECURITY.md, ADR-011 | E16: clean verify -Pmigration-tests-external | 265 unitarias + 57 IT aprobadas; JWT/HTTP/DB y A15 puerta de recursos/A16 HTTP | Keycloak real y despliegue pendientes, fuera de verificación local | T08: reutilizar autorización para solicitudes |
+| T08 | DONE | T06,T07 DONE | request/application, request/api, PostgresRequestStore, RequestJson, PersistenceAccessConfiguration, RequestCreationIT, REQUESTS.md, ADR-012 | E17: clean verify -Pmigration-tests-external | 268 unitarias + 66 IT aprobadas; creación atómica, replay, concurrencia, ETag/ACL | Ninguno para implementación local; despliegue pendiente | T09: aceptar eventos idempotentes |
+| T09 | DONE | T08 DONE | execution/api, Events/EventStore, EventPolicy, PostgresEventStore, EventAcceptanceIT, EVENTS.md, ADR-013 | E18: clean verify -Pmigration-tests-external | 268 unitarias + 76 IT aprobadas; A07–A09 aceptación durable, guardias, roles, rollback | Ninguno para aceptación local; worker y despliegue pendientes | T10: worker, leases y consulta de ejecución |
+| T10 | DONE | T09 DONE | WorkerStore/PostgresWorkerStore, WorkerEngine/WorkerLoop, ExecutionController, V3, WorkerIT, WORKER.md, ADR-014 | E19: suite completa + verificación final WorkerIT | 268 unitarias y 93 IT distintos aprobados; worker final 17 casos, fencing/checkpoints/recuperación | Ninguno para worker local; adapters T11 y retry T12 pendientes | T11: handlers e integraciones HTTP |
+| T11 | DONE | T10 DONE | action/infrastructure, integration/infrastructure, PostgresWorkerStore, mocks HTTP, examples/integrations, INTEGRATIONS.md, ADR-015 | E20: clean verify -Pmigration-tests-external | 301 unitarias + 99 IT = 400 aprobadas; 0 fallos/errores/omisiones | Integración real de proveedores pendiente de contratos/credenciales; sin bloqueo local | T12: reintentos y conciliación |
+| T12 | DONE | T11 DONE | RetryDelay, WorkerEngine/Store, Resolutions/PostgresResolutionStore, ResolutionController, RetryReconciliationIT, RECONCILIATION.md, ADR-016 | E21: clean verify -Pmigration-tests-external | 303 unitarias + 116 IT = 419 aprobadas; retries, resolución atómica y 5 caídas de JVM reales | Ninguno local; proveedores reales fuera de alcance | T13: documentos y tareas para front |
+| T13 | DONE | T11,T12 DONE | AvailableEvents/Policy/Store/Controller, DocumentMetadata, WorkerEngine, AvailableEventsIT/DocumentTasksIT, AVAILABLE_EVENTS.md, ADR-017 | E22: clean verify -Pmigration-tests-external | 308 unitarias + 137 IT = 445 aprobadas; A11/A20 y tareas/bloqueos verificados | Ninguno local; documental real fuera de alcance | T14: flujos demostrativos v1/v2 |
+| T14 | DONE | T05,T06,T13 DONE | examples/flows, examples/integrations, demo-flows.http, DemoFlowTest/DemoFlowsIT, ADR-018 | E23: verify -Pmigration-tests-external -Dit.test=DemoFlowsIT,DocumentTasksIT,HttpIntegrationIT | 311 unitarias + 29 IT = 340 aprobadas; A10/A18, fronteras y dos tipos completos | Proveedores reales fuera del alcance local; simulate registrado para T16 | T15: auditoría y observabilidad |
+| T15 | DONE | T09,T10,T11,T12,T13,T14 DONE | audit/api, HistoryStore, QueueMetrics, CommandObservationFilter, Worker/HTTP, ObservabilityTest/IT, OBSERVABILITY.md, ADR-019 | E24: clean verify -Pmigration-tests-external + check final cursor | 314 unitarias + 163 IT = 477; ajuste final 4 unitarias + 1 IT aprobados, sin fallos ni omisiones | Ninguno local; Docker/proveedores reales fuera de evidencia | T16: verificación integral |
+| T16 | BLOCKED | T15 DONE | FlowSimulation/SimulationController, TransitionEvaluator, SimulationTest/IT, acceptance-cases.json, verify_acceptance.py, CI, ACCEPTANCE.md, ADR-020 | E25: clean verify externo + gate A01–A20 + prueba Docker | 326 unitarias + 167 IT = 493 aprobadas; A01–A20 PASS externo; 2 tests Python aprobados | Testcontainers no encuentra entorno Docker válido; CI remoto no ejecutado | Habilitar motor Docker autorizado y ejecutar clean verify + gate --require-testcontainers |
| T17 | PENDING | T16 | Dockerfile, compose.yaml, README, RUNBOOK, examples/http, CI | Pendiente: Build de imagen, compose, verify y demo completa de sección 16 | No ejecutado | Dependencias sin completar | Construir imagen y Compose completos |
## E11 — Adaptación PostgreSQL 9.6.2 — 2026-10-26 13:23 (America/Mexico_City)
@@ -96,9 +96,339 @@ Con la configuración del repositorio, readiness incluye PostgreSQL. Esta prueba
- Siguiente paso: completar .env, ejecutar docker compose config --quiet y docker compose up --build -d; comprobar readiness y logs. T01 continúa BLOCKED y T17 PENDING hasta satisfacer sus criterios completos.
1. Leer este registro, especificación y cambios del repositorio.
-2. T01/T02 DONE. Preparar T03: migraciones desde cero, constraints, índices, triggers y entorno PostgreSQL/Testcontainers.
-3. Registrar evidencia de T03 y mantener una única tarea principal IN_PROGRESS. No repetir verificaciones aprobadas sin cambios o fallos que lo justifiquen.
+2. T01–T10 DONE. Siguiente tarea habilitada: T11, handlers e integraciones HTTP.
+3. Registrar evidencia de la siguiente tarea y mantener una única tarea principal IN_PROGRESS. No repetir verificaciones aprobadas sin cambios o fallos que lo justifiquen.
## Plantilla de evidencia adicional
`Fecha | ID | Archivos | Comando/check exacto | Resultado y reporte | Bloqueo | Próximo paso`
+
+## E12 — Cierre T03 — 2026-10-26 14:12 (America/Mexico_City)
+
+- Entorno autorizado: server02:30432/orquestador_test, PostgreSQL 9.6.2 real; conexión inicial de solo lectura confirmó nombre y versión. No se modificó la base orquestador ni se desplegó el servicio remoto.
+- Archivos: V1__orchestrator_tables.sql, V2__immutability_and_runtime_privileges.sql, DatabaseMigrationIT.java, pom.xml, application.yaml, compose.yaml, .env.example, probe de compatibilidad y documentación.
+- Comando: `.\mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`, JDK 21.0.12.1 y credenciales MIGRATION_TEST_DB_* en entorno de proceso.
+- Resultado final: BUILD SUCCESS, 55.245 s, 92 pruebas Surefire y 40 Failsafe, cero fallos/errores/omisiones. Reportes: target/surefire-reports/com.organizacion.orchestrator.ContractValidationTest.txt y target/failsafe-reports/com.organizacion.orchestrator.DatabaseMigrationIT.txt.
+- V1/V2 aplicadas desde esquema vacío y segunda ejecución sin migraciones pendientes. Verificadas diez tablas, FKs, JSONB/checks, idempotencia, índice activo incluyendo conciliación, flujos publicados/retirados inmutables, históricos append-only, rol runtime sin DDL ni cambios de auditoría, rollback atómico, rollback de migración fallida, carrera de dos inserciones y SKIP LOCKED.
+- La primera corrida encontró una comparación textual false/f en la prueba JDBC de permisos; se corrigió con cast explícito a text. La suite completa final pasó, incluyendo los checks restantes del rol.
+- El ERROR de Flyway V999 es intencional: el test comprueba que DDL e historial se revierten. V999 es temporal, no se entrega como migración.
+- Esquema final aislado t03_304ad66fc7c3485ca63e474f2fe76c19 eliminado por AfterAll; rol temporal revertido y su ausencia comprobada. No se guardaron contraseñas en archivos del proyecto.
+- Testcontainers 2.0.5/PostgreSQL 9.6.24 preparado como ruta normal de verify/CI; ejecución local no realizada por ausencia de Docker. ADR-007 documenta alternativa real externa y límites.
+- Siguiente: T04 PENDING con dependencias satisfechas. T06 conserva la validación semántica/publicación y T07 la autorización por recurso.
+
+## E13 — Cierre T04 — 2026-10-26 15:52 (America/Mexico_City)
+
+- Criterios previos: records y errores estables; DTO separado de dominio/persistencia; puertos DecisionEvaluator, ActionHandler y FlowDefinitionValidator; dominio sin dependencias HTTP/JPA. Pruebas pertinentes: invariantes e inmutabilidad, recompilación aislada y compatibilidad DTO/schema.
+- Implementación: 30 archivos de dominio/puertos bajo shared, flow, request, execution, decision y action; un DTO de validación en flow/api. JSON sellado e inmutable con BigDecimal y null/ausencia distintos; AST y acciones tipadas; identidad por recurso; snapshots de flujo, solicitud, ejecución y checkpoint. Ver DOMAIN_MODEL.md y ADR-008.
+- Comando final: `.\mvnw.cmd --batch-mode --no-transfer-progress test`, JAVA_HOME configurado al JDK local 21.0.12.1.
+- Resultado: BUILD SUCCESS, 22.456 s, 134 tests (92 contractuales existentes + 39 DomainModelTest + 1 DomainArchitectureTest + 2 DomainBoundaryTest), cero fallos/errores/omisiones. Reportes en target/surefire-reports.
+- Arquitectura: los 30 archivos domain/application/port recompilaron con classpath aislado y únicamente java.base, sin fuentes API/infraestructura. Se prohíben además referencias java.net/java.sql. No se añadió dependencia Maven.
+- Verificado: copias inmutables de colecciones, precisión decimal, null/ausencia, pointers permitidos, orden de AST/reglas/acciones, políticas de reintento acotadas, publicación/retiro, ejecución activa y conciliación, lease consistente, resultado/checkpoint exitoso obligatorio, separación de fallo técnico/resultado negativo y hashes compatibles con DB/API. DTO de validación comprobado contra api.schema.json.
+- Primera ejecución: 129 pruebas aprobadas. Revisión posterior alineó hashes a hexadecimal minúscula y añadió el DTO y cinco casos adicionales; la ejecución final de 134 pruebas aprobó todo.
+- Alcance: sin cambios a SQL, contratos, configuración de despliegue o credenciales. No se accedió a PostgreSQL ni se repitieron sus IT de E12, al no cambiar persistencia. No se desplegó el servicio. Los puertos no tienen implementaciones simuladas; T05/T06/T11 conservan sus criterios funcionales y A01–A20 siguen pendientes.
+- Siguiente tarea: T05 PENDING con dependencia satisfecha. No hay tarea principal IN_PROGRESS.
+
+## E14 — Cierre T05 — 2026-10-26 17:05 (America/Mexico_City)
+
+- Criterios previos: once operadores, JSON Pointer, BigDecimal, tipos estrictos, null/ausencia, cortocircuito, límites, UNIQUE/FIRST y ambigüedad. Verificar scoring 71/70/61/60 y solapamientos simples, con dominio independiente de infraestructura.
+- Archivos: PureDecisionEvaluator, ConditionEvaluator, JsonPointerResolver, AstLimits, EvaluationLimits, IntervalOverlapAnalyzer; DecisionEvaluatorTest, JsonPointerResolverTest, IntervalOverlapAnalyzerTest; ampliación de DomainArchitectureTest; documentación y ADR-009.
+- Comando final: `.\mvnw.cmd --batch-mode --no-transfer-progress test`, JDK 21.0.12.1. BUILD SUCCESS, 13.414 s. 216 pruebas, cero fallos/errores/omisiones: 92 contratos + 56 decisiones + 17 pointers + 9 intervalos + 39 dominio + 1 arquitectura + 2 DTO/schema.
+- Reportes: target/surefire-reports/com.organizacion.orchestrator.DecisionEvaluatorTest.txt, JsonPointerResolverTest.txt e IntervalOverlapAnalyzerTest.txt (mismo prefijo de paquete); pruebas previas también aprobadas.
+- A04: fixture contractual scoring.json produce N/M/M/MANUAL para 71/70/61/60, además de fracciones precisas y valores fuera del rango ilustrativo. No se impuso rango universal 0–100.
+- A05: fixtures de proveedor con score ausente/string rechazan BureauResult; si llegan al evaluador producen EVALUATION_ERROR, nunca default. Null también falla. No es aún prueba de adapter HTTP/proveedor.
+- A06: varias coincidencias UNIQUE producen AMBIGUOUS_DECISION, incluso con igual output. FIRST conserva precedencia y regla aplicada; errores no se convierten a negocio negativo. Evaluación pura sin escrituras.
+- Verificados límites exactos de 20 niveles/1000 nodos, suma de reglas y ramas no ejecutadas, entrada de 50000 niveles rechazada sin stack overflow, comparación estructural/IN acotados, cortocircuito ordenado y 40 evaluaciones concurrentes con la misma instancia/contexto intacto.
+- Intervalos: extremos abiertos/cerrados, conjunciones, intervalos vacíos y operandos invertidos. Fuera del subconjunto se informa alcance desconocido; no se afirma satisfacibilidad general. Arquitectura recompila también el evaluador application con java.base y sin I/O/HTTP/JPA.
+- Primera corrida: 215 pruebas aprobadas; se añadió rechazo temprano por número de reglas y una prueba de frontera exacta de nodos. Corrida final: 216 aprobadas.
+- Sin cambios SQL, contratos REST/DSL ni dependencias; no se accedió a PostgreSQL ni proveedores, no hubo despliegue. Los IT PostgreSQL de E12 no se repitieron porque no cambió persistencia. A04–A06 están cubiertos al nivel T05; la aceptación integral REST permanece en T16.
+- Siguiente: T06 PENDING con dependencias satisfechas. Ninguna tarea principal IN_PROGRESS.
+
+## E15 — Cierre T06 (servicios internos) — 2026-10-26 21:58 (America/Mexico_City)
+
+- Criterios previos: validar grafo/schemas/roles/capacidades/mappings; edición con revisión; publicar definición inmutable y checksum con contratos/perfiles fijados; activar/retirar con bloqueos y auditoría atómica. Verificar fallos y carreras en PostgreSQL real. ADR-010 explicita la separación entre servicio interno T06 y exposición REST segura T07.
+- Archivos: PublicationCatalog, FlowJsonCodec, ConfiguredFlowValidator, FlowPreparation/FlowStore, CatalogFlowPreparation, FlowAdministration/FlowValidationException, PostgresFlowStore, FlowFixtures/FlowValidationTest/FlowPublicationIT, DomainArchitectureTest, pom.xml y documentación.
+- Comando final: `.\mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`, JAVA_HOME en JDK 21.0.12.1, credenciales MIGRATION_TEST_DB_* solo en entorno de proceso.
+- Resultado final: BUILD SUCCESS, 2:07 min. 258 pruebas Surefire y 51 Failsafe, cero fallos/errores/omisiones. FlowValidationTest: 42 casos; FlowPublicationIT: 11 casos reales (47.86 s); DatabaseMigrationIT: 40 casos (58.63 s). Reportes: target/surefire-reports y target/failsafe-reports/com.organizacion.orchestrator.FlowPublicationIT.txt / DatabaseMigrationIT.txt.
+- Validación: grafo, terminales/sumideros/alcanzabilidad, IDs/parejas únicas, roles, schemas cerrados, catálogo/perfiles, NO_OP, inputs y resultados previos, tipos, updates solapados, protección de datos de proveedor, tareas documentales, decisiones y límites. Decoder completo del DSL y roundtrip de fixtures T02, rechazo de claves duplicadas/JSON excesivo/referencias externas/Unicode incompatible, canonicalización y hash estable.
+- Persistencia: ciclo crear/editar/publicar/activar/retirar, revisiones obsoletas, inmutabilidad de servicio y DB, permisos técnicos internos, contratos/perfiles fijados, retiro sin alterar instancias existentes y limpieza auditada de referencias activas. Cada escritura administrativa se revirtió ante un fallo SQL real inducido en admin_audit.
+- Concurrencia con barreras: dos publicadores; editar/publicar; dos activaciones; activar/retirar; asignar antes del retiro; retiro antes de asignación. Ninguna asignación posterior al retiro ni cambio parcial. T08 debe conservar la misma transacción para solicitud/idempotencia/auditoría, según FLOW_PUBLICATION.md.
+- A16 acreditado a nivel API interna y DB; comprobación REST/JWT no ejecutada ni anunciada como aprobada. A17 acreditado con puerta de asignación e inserción real de solicitudes en transacción. T07/T08/T16 completarán las rutas y el recorrido HTTP. No se inició T07.
+- Aislamiento: únicamente orquestador_test. Último esquema T06: t06_ef065d4bc2c34f5dbbe3f839e7d1d96a, eliminado por AfterAll; también se limpió el esquema propio T03. Trigger de fallo solo en esquema de prueba. No se tocó orquestador ni se hizo redeploy.
+- La primera solicitud de ejecución remota agotó el plazo de revisión automática y no se inició. El reintento permitido fue aprobado y pasó (255 unitarias + 50 IT); tras ampliar controles/auditoría y añadir el orden inverso de la carrera, la corrida final pasó con 309 pruebas. No queda bloqueo de aprobación.
+- Artefacto: target/orquestador-nmp-0.1.0-SNAPSHOT.jar, SHA256 2E60C16853ECB1118922294996DF1041CA79A9CC9D71C394FD8C848EAF53B5DA. Inspección ZIP confirmó tres schemas empaquetados, networknt 1.5.9 y Jackson 2.21.7 junto al Jackson 3.1.7 de Boot. No se ejecutó Docker ni arranque/redeploy de este JAR.
+- Decisión técnica: JDBC transaccional para administración, detrás de puertos puros, en lugar del JPA propuesto para estos agregados; motivo/alternativas documentados en ADR-010. No se añadieron migraciones ni se cambiaron contratos REST. No hay proveedor real ni catálogo simulado registrado en producción.
+- Siguiente: T07 PENDING, dependencias satisfechas. Ninguna tarea principal IN_PROGRESS.
+
+
+## E16 — Cierre T07 — 2026-10-27 02:47 (America/Mexico_City)
+
+- Criterios: JWT firmado (firma/issuer/audience/exp), roles técnicos y scope, puerta de propiedad para solicitud/ejecución/documento/auditoría, runtime DB restringido y seis rutas administrativas T06 con A16 por HTTP. ADR-011 registra decisiones de alcance y autorización.
+- Archivos: SecurityConfiguration, JwtActorMapper, RuntimeRoleVerifier, ResourceAccess/ResourceOwnershipStore/ResourceOwner, PostgresResourceOwnershipStore, FlowAdministrationController/FlowView, CorrelationFilter/ProblemResponse/ApiExceptionHandler, PublicationCatalogLoader/FlowAdministrationConfiguration, config/, compose.yaml, application.yaml, .env.example, SecurityPolicyTest, SecurityHttpIT, ResourceProbe exclusivo de test, DomainArchitectureTest y SECURITY.md.
+- Comando final: `.\mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`, JDK 21.0.12.1; credenciales MIGRATION_TEST_DB_* solo en entorno de proceso. BUILD SUCCESS, 2:10 min, finalizado 2026-10-27T02:47:52-06:00.
+- Resultado: 265 pruebas Surefire + 57 Failsafe = 322, cero fallos/errores/omisiones. IT: DatabaseMigrationIT 40, FlowPublicationIT 11, SecurityHttpIT 6. SecurityPolicyTest 7. Reportes target/surefire-reports y target/failsafe-reports.
+- JWT RSA real y JWKS HTTP local: firma ajena, issuer/audience incorrectos, expiración, nbf futuro, ausencia de exp/sub/ámbito y token malformado/ausente denegados. Roles realm/cliente correcto aceptados; otros clientes y OAuth scope no conceden permisos. Ámbito administrativo ajeno denegado.
+- HTTP administrativo: crear/validar/publicar/activar/retirar; rol editor no publica; ETag y precondiciones 428/400/412; editar publicado 409 (A16); JSON duplicado/malformado 400, schema inválido 422, cuerpo excesivo 413. Health anónimo 200, ausencia de cookies de sesión en errores autenticados.
+- A15: los cuatro recursos persistidos resuelven propietario/ámbito a través de PostgreSQL. Dueño del mismo ámbito accede; mismo dueño de otro ámbito, otro dueño incluso FLOW_ADMIN y recurso inexistente reciben 404. Conciliación exige operador y ámbito, con delegación explícita de propietario. Se verifica por HTTP con ResourceProbe, controlador solo de test fuera del escaneo productivo, no son las rutas funcionales futuras.
+- DB: conexión independiente con LOGIN temporal no elevado, concesiones V2, migración con propietario distinto; RuntimeRoleVerifier acepta runtime y rechaza propietario. DDL, DELETE admin_audit y lectura flyway_schema_history rechazados por PostgreSQL real.
+- Aislamiento: únicamente orquestador_test; schema final t07_64e1695df2154c3ba0ad28c4530c0ef0 y rol t07_runtime_64e1695df2154c3ba0ad28c4530c0ef0 eliminados por AfterAll. Esquemas propios T03/T06 también limpiados. No se modificó orquestador, no se desplegó ni se invocaron proveedores.
+- Corrida inicial aprobada: 265 unitarias; integración dirigida 4 casos aprobados el 2026-10-26. Se amplió con recursos persistidos y roles de cliente; la corrida final incluye esos 6 casos y todas las regresiones. El wrapper requirió ejecución fuera del sandbox para acceder a la caché/JDK; revisión automática aprobó ejecución. No queda bloqueo técnico.
+- Configuración local: administración deshabilitada por defecto; al habilitar exige usuario DB restringido, catálogo y lista ADMIN_BUSINESS_SCOPES. business_scope es el claim por defecto configurable, pendiente de confirmar contra un JWT real de controlms. Verificar emisión real Keycloak y desplegar no se presentan como pruebas aprobadas.
+- Sin nuevas migraciones ni cambio de idempotencia/versionado. Compose conserva un único servicio y PostgreSQL externo. Simulación se mantiene pendiente T14 y resolución funcional T12. T08 debe integrar la puerta ResourceAccess y desacoplar su wiring del interruptor administrativo conservando la verificación DB. Siguiente: T08 PENDING; ninguna tarea principal IN_PROGRESS.
+
+## E17 — Cierre T08 — 2026-10-27 03:03 (America/Mexico_City)
+
+- Criterios previos: resolver/fijar flujo publicado activo, validar initialDataSchema y dataSchema, idempotencia por actor/ámbito, solicitud/auditoría/ledger atómicos, consulta con ETag/ejecución activa y autorización. Pruebas de carreras reales, rollback y HTTP firmado; no cerrar por compilación.
+- Archivos: Requests/RequestStore/RequestSnapshot, RequestJson/PostgresRequestStore/RequestConfiguration, RequestController/RequestView, PersistenceAccessConfiguration, ajustes FlowAdministrationConfiguration/ApiExceptionHandler, flags application.yaml/compose/.env.example; RequestJsonTest, RequestCreationIT, fixture SecurityHttpIT y DomainArchitectureTest. Contrato OpenAPI/README, REQUESTS.md, SECURITY.md, ADR-012 y examples/http/requests.http actualizados.
+- Comando final: `.\mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`; JDK 21.0.12.1, MIGRATION_TEST_DB_* solo en entorno de proceso. BUILD SUCCESS, 2:34 min, finalizado 2026-10-27T03:03:36-06:00.
+- Resultado: 268 unitarias Surefire + 66 IT Failsafe = 334; cero fallos/errores/omisiones. IT: DatabaseMigrationIT 40, FlowPublicationIT 11, RequestCreationIT 9, SecurityHttpIT 6. Reportes target/surefire-reports y target/failsafe-reports.
+- HTTP: 201/Location/ETag, GET autorizado, RequestView/Problem contra schema contractual; replay idéntico con respuesta original después de activar v2, retirar v1 o avanzar revisión; GET conserva versión fijada y expone ejecución activa/ETag actuales. Conflicto 409 por otro contenido; clave independiente por actor/ámbito; scope/owner ajenos 404, sin JWT 401. Administración apagada 403 mientras solicitudes funcionan.
+- Entrada: command cerrado, identidad cliente rechazada, ambos schemas de datos, tipos estrictos, score no permitido, tamaño UTF-8/canónico, JSON duplicado y decimal preciso. Ausencia de clave 400, schema 422, tamaño 413, tipo inexistente 404, tipo deshabilitado/sin activo 409. Fallos no consumen clave; replay no depende de disponibilidad del catálogo activo.
+- Concurrencia real: dos POST de igual clave generan una única solicitud/respuesta; comandos distintos en carrera producen 201/409. Barreras dentro de la asignación comprueban retención de locks flujo/tipo hasta commit ante activación y retiro. Solicitud aceptada conserva versión anterior; posteriores usan nueva versión o rechazan tipo retirado. Regresiones de carreras T06 también aprobadas.
+- Atomicidad: triggers exclusivos de prueba fallan realmente al insertar auditoría o ledger. No queda solicitud/ledger parcial; reintento tras fallo de auditoría funciona. Implementación JDBC comparte DataSource/READ COMMITTED con FlowStore y usa advisory lock transaccional más clave única completa; ADR-012 registra elección y semántica de replay.
+- Aislamiento: únicamente orquestador_test. Se reutilizó fixture T07 con schema t07_704fbd4eaf8e4f649d08b40a78a6bf4c y rol LOGIN t07_runtime_704fbd4eaf8e4f649d08b40a78a6bf4c, eliminados por AfterAll de cada suite; T07 recrea/limpia esos objetos propios para regresión. T03/T06 limpian sus esquemas propios. No se tocó orquestador ni se desplegó en server02:8787.
+- Primera corrida: 268 unitarias aprobadas; integración dirigida inicial de 7 casos aprobada (51.694 s). Se añadieron validación de esquema inicial y retiro concurrente; corrida final de 9 casos T08 y todas las regresiones aprobada. No quedan fallos conocidos ni bloqueos externos de prueba.
+- No se necesitaron nuevas migraciones: V1/V2 contienen tabla idempotente, auditoría, permisos y restricciones. No hay proveedores ni mocks de resultados de negocio. JWT real firmado con JWKS local, no emisión real de controlms. Docker no ejecutado; Compose solo servicio con PostgreSQL externo y REQUEST_API_ENABLED=false por defecto. Para habilitar, configurar runtime restringido; independiente de administración.
+- Siguiente: T09 PENDING con dependencia DONE. Ninguna tarea principal IN_PROGRESS. Eventos, worker y recorrido integral siguen pendientes; no se anuncian cubiertos por T08.
+
+## E18 — Cierre T09 — 2026-10-27 03:36 (America/Mexico_City)
+
+- Criterios previos: bloquear solicitud autorizada, replay antes de revisión, If-Match/rol/schema/guardia, ejecución PENDING + revisión + EVENT_ACCEPTED atómicos, respuesta 202/Location. Verificar A07–A09 con PostgreSQL real y rollback, sin HTTP de proveedores.
+- Archivos: AcceptedEvent/EventPolicy, Events/EventStore, PostgresEventStore/EventConfiguration, EventController, ApiExceptionHandler; EventAcceptanceIT y ampliación DomainArchitectureTest. Contrato OpenAPI, EVENTS.md, ADR-013, ejemplos HTTP y documentación actualizados. No se reemplazó trabajo previo.
+- Comando final: `.\mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`, JDK 21.0.12.1 y credenciales MIGRATION_TEST_DB_* solo en entorno de proceso. BUILD SUCCESS, 7:31 min; finalizado 2026-10-27T03:36:50-06:00.
+- Resultado: 268 unitarias Surefire + 76 IT Failsafe = 344, cero fallos/errores/omisiones. IT: DatabaseMigrationIT 40, EventAcceptanceIT 10, FlowPublicationIT 11, RequestCreationIT 9, SecurityHttpIT 6. Reportes target/surefire-reports y target/failsafe-reports, particularmente com.organizacion.orchestrator.EventAcceptanceIT.txt.
+- A07: reintento devuelve mismo executionId y status actual sin nuevas escrituras, incluso tras cambiar estado/revisión/rol. Acceso actual y sujeto original se comprueban siempre. Nueva instancia del adapter recupera replay desde PostgreSQL; no se anuncia reinicio del proceso. Orden de JSON y escala decimal equivalente no alteran hash; revisión esperada sí lo altera.
+- A08: clave repetida con evento/datos/revisión distintos produce 409 IDEMPOTENCY_CONFLICT. Carrera simultánea con payloads distintos produce un 202 y un 409; no duplica ejecución ni revisión.
+- A09: dos envíos idénticos simultáneos comparten ejecución; dos claves con igual revisión producen 202/412. PENDING/RUNNING/RETRY_WAIT/RECONCILIATION_REQUIRED impiden nuevo evento con revisión actual (409 REQUEST_BUSY); replay sigue permitido.
+- Seguridad/validación: JWT ausente 401, owner/scope ajenos 404, rol ajeno 403, identidad/roles en comando rechazados, campos de proveedor fuera del schema 422. Guardia falsa GUARD_NOT_SATISFIED y valor ausente EVALUATION_ERROR, ambos 422 sin escrituras. If-Match ausente 428, inválido 400, obsoleto 412; payload excesivo 413. AcceptedEvent y Problem comprobados contra schemas.
+- Atomicidad: fallos SQL reales inducidos al insertar ejecución, actualizar revisión e insertar auditoría revierten todo; auditoría previa de creación se conserva. Reintento tras fallo de auditoría funciona. Error SQL por HTTP retorna 503 seguro. Estado y data de negocio permanecen intactos al aceptar; accepted_revision es la nueva revisión (If-Match + 1).
+- Flujo fijado RETIRED sigue aceptando eventos de instancias previas. Caso con INVOKE_INTEGRATION configurado acepta PENDING sin checkpoints, resultado ni actualización de datos: no existe adapter HTTP de proveedor invocado. Los cambios SQL a estados técnicos usados por pruebas son fixtures explícitos, no un worker implementado.
+- Aislamiento: solo orquestador_test. Fixture compartido T07/T08 usa schema t07_b8dc4f7d95774fbc92a43745b161065d y LOGIN t07_runtime_b8dc4f7d95774fbc92a43745b161065d, creados/eliminados por cada suite propia. T03/T06 también limpian sus esquemas. No se tocó orquestador, no hubo despliegue ni uso de datos personales/proveedores.
+- Corridas previas: 268 unitarias aprobadas; IT dirigida inicial 8 casos aprobados en 2:09 min. Se añadieron carrera con payload distinto/canonicalización y flujo con integración declarada; corrida final de 10 casos T09 y regresiones aprobada. No quedan fallos conocidos.
+- JAR generado: target/orquestador-nmp-0.1.0-SNAPSHOT.jar; SHA256 552B5273EB904EA6ED447BD48B18C98C0E33ED97FFA0353A4CFD2ED5F1609F16. Inspección ZIP confirma ausencia de clases/controladores de prueba. git diff --check aprobado.
+- Sin nuevas migraciones; usa V1/V2 y REQUEST_API_ENABLED existente con runtime restringido. ADR-013 documenta replay, accepted_revision y alcance. GET /executions/{id} queda como subtarea explícita T10; Location mantiene contrato objetivo. GET de solicitud ya muestra activeExecutionId, pero sin worker los eventos permanecen PENDING. A07–A09 de efectos externos requieren T10/T11/T16.
+- Siguiente: T10 PENDING con dependencia DONE. Ninguna tarea principal IN_PROGRESS. No se inició worker ni procesamiento externo.
+
+## E19 — Cierre T10 — 2026-10-27 08:52 (America/Mexico_City)
+
+- Criterios previos: reclamar con SKIP LOCKED, leases por tiempo DB, fencing en todas las escrituras, checkpoints/reuso y recuperación inicial; dos workers, lease vencido, retorno tardío, rollback final y GET de ejecución autorizado. No confundir executor local con cola durable ni declarar providers implementados.
+- Archivos: WorkerState, WorkerStore/WorkerActions, PostgresWorkerStore, WorkerEngine/WorkerLoop/WorkerConfiguration, ExecutionController, PersistenceAccessConfiguration; V3__action_effect_checkpoint.sql, configuración application.yaml/compose/.env.example; WorkerIT y expectativas V3 de DatabaseMigrationIT; WORKER.md, ADR-014, contrato, ejemplos y documentación.
+- Suite completa: `.\mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`; BUILD SUCCESS, 5:15 min, finalizado 2026-10-27T08:49:43-06:00. 268 unitarias + 91 IT (WorkerIT 15), cero fallos/errores/omisiones. Migraciones 40, eventos 10, publicación 11, creación 9, seguridad 6 y worker 15.
+- Ajuste posterior acotado: fallar controladamente datos finales excesivos y validar tipo de capability; actualización contractual/documental. Se añadieron dos pruebas de worker (límite de datos y checkpoint faltante) y se verificó el código final con `.\mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external '-Dit.test=WorkerIT'`. BUILD SUCCESS, 2:36 min, finalizado 2026-10-27T08:52:38-06:00; 268 unitarias + 17 WorkerIT, sin fallos/errores/omisiones. No se repitieron IT ajenos al ajuste ya aprobados. Evidencia vigente: 361 casos distintos = 268 unitarias + 93 IT; no se presenta como una sola corrida final de 361.
+- Entorno: JDK 21.0.12.1 y MIGRATION_TEST_DB_* solo en variables de proceso. Reportes target/surefire-reports y target/failsafe-reports; WorkerIT.txt refleja los 17 casos finales.
+- Reclamación: dos stores/owners concurrentes reciben ejecuciones distintas; una fila bloqueada se omite; RETRY_WAIT futuro no se reclama hasta tiempo DB vencido. Renovación no revive lease vencido; recuperación incrementa fencing_token. Token antiguo no puede renovar, cargar, iniciar/guardar checkpoint, fallar ni confirmar.
+- Procesamiento: ejecución sin acciones completa estado/datos/revisión/resultado/auditoría; decisión T05 aplica updates y conserva regla aplicada. Resultado del worker viejo que retorna después del nuevo no sobrescribe datos. Finalización rechaza snapshot/revisión obsoletos y checkpoints faltantes. Límites finales producen fallo controlado, no recuperación infinita.
+- Checkpoints: caída simulada tras primer éxito conserva ese resultado, reintenta solo la acción incompleta con clave estable e input_hash compatible. Reconstrucción de repositorio/engine recupera desde PostgreSQL. Cambio de hash bloquea reuso. READ_ONLY/IDEMPOTENT_WRITE recuperables; NON_IDEMPOTENT_WRITE incierto no se repite y exige conciliación. Doble de proveedor idempotente registra dos llamadas pero un efecto por clave; esto no acredita un proveedor real.
+- Atomicidad: fallo SQL inducido al auditar finalización revierte datos/estado/revisión/resultado finales; checkpoint de acción permanece SUCCEEDED y recuperación no repite llamada. Escritura exitosa seguida de conflicto final conserva solicitud bloqueada para conciliación. Adapter ausente falla explícitamente INTEGRATION_UNAVAILABLE, sin fabricar éxito.
+- Scheduler real de prueba: reclama trabajo, renueva dos veces durante acción bloqueada por barrera, finaliza y se detiene; solicitud posterior queda PENDING. Heartbeat cancelado no interrumpe otro trabajo al reutilizar thread. Apagado drena con renovación hasta 20 s y deja trabajo restante para recuperación por lease.
+- GET /executions/{id}: Pending/Succeeded/Failed contra schema ExecutionView, owner/scope ajenos 404, sin JWT 401. No se retornan snapshots de identidad, inputs, lease_owner o payload crudo de proveedor. Location T09 ya es consultable.
+- Migración V3: persiste clasificación de efecto confiable; V1/V2 intactas, checkpoints previos conservadoramente NON_IDEMPOTENT_WRITE. Tres migraciones aplicadas/validadas e idempotentes; pruebas de permisos/rollback previas aprobadas con V3.
+- Aislamiento: exclusivamente orquestador_test, fixtures temporales propios. Suite final WorkerIT: schema t07_b639d9ea7fe94294beead496e8b1d64b y rol t07_runtime_b639d9ea7fe94294beead496e8b1d64b eliminados por AfterAll. Suite completa usó t07_3a45246f4c704a54b855f3c40003427d más schemas propios T03/T06, también limpiados. No se tocó orquestador ni se desplegó.
+- Límites explícitos: SimulatedCrash abandona intento y se reconstruye repositorio; no es reinicio de JVM. Adapters/proveedor deduplicador son dobles de test, no registros productivos. T11 implementará adapters reales/mocks HTTP; T12 política completa de errores transitorios/backoff/Retry-After/conciliación. T10 reconoce RETRY_WAIT y recuperación de lease, pero no anuncia retry HTTP completo. Docker y Keycloak reales no ejecutados en esta tarea.
+- Defaults: WORKER_ENABLED=false, lease 60 s, renovación 15 s, concurrencia/lote 10, polling 500 ms. Puede correr sin API/admin, exige runtime DB restringido. Flujos sin acciones funcionan; integraciones no registradas fallan, por lo que se mantiene apagado para esos flujos hasta T11.
+- Siguiente: T11 PENDING, dependencia DONE. Ninguna tarea principal IN_PROGRESS; no se inició integración de proveedor.
+
+## E20 — Cierre T11 — 2026-10-27 11:40 (America/Mexico_City)
+
+- Criterios previos: registro de handlers confiable, contratos/perfiles fijados, POST HTTP acotado, separación negocio/error, NO_OP solo simulación y referencias documentales verificadas. Pruebas HTTP reales contra mocks loopback y PostgreSQL real; sin proveedores ni despliegue.
+- Archivos: HttpRoutes/BoundedHttp/HttpIntegrationHandler/IntegrationConfiguration, RegisteredWorkerActions/RegisterDocumentHandler/NoOpSimulationHandler, PublicationCatalogConfiguration, WorkerConfiguration y PostgresWorkerStore.save; config/integration-routes.json, application.yaml/compose/.env.example; HttpProviderFixture/HttpIntegrationTest/RegisterDocumentTest/HttpIntegrationIT, examples/integrations, INTEGRATIONS.md, WORKER.md, README y ADR-015.
+- Prueba dirigida final: `mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external -Dit.test=HttpIntegrationIT -Dtest=HttpIntegrationTest,RegisterDocumentTest`; BUILD SUCCESS, 2:13 min, finalizado 2026-10-27T11:25:47-06:00. 33 tests + 6 IT, cero fallos/errores/omisiones.
+- Correcciones durante prueba: el fixture decimal usaba Double y se sustituyó por parser decimal estricto; el JSON documental del fixture se corrigió a inputs.documentId conforme al DSL existente. Las pruebas finales dirigidas pasan; no se alteró el contrato para aceptar el fixture.
+- Transporte: JSON duplicado/malformado/schema inválido rechazado; precisión decimal, mappings, headers de perfil/clave estable, bearer desde entorno, secreto ausente sin envío, versiones no fijadas/ajenas sin envío, 404 explícito, 429/5xx, rechazo de redirecciones, límite de memoria y timeout del cuerpo después de headers. Escritura incierta no se transforma en resultado de negocio.
+- Negocio: cliente encontrado persiste datos y avanza START→END; cliente no encontrado SUCCEEDED/CLIENTE_NO_ENCONTRADO conserva START; timeout/503/contrato inválido FAILED sin customer ni outcome negativo. DOCUMENTO_VALIDA INVALID es resultado de negocio; BURO_CONSULTA exige score numérico. Son contratos propuestos con datos sintéticos, no integración real aprobada.
+- Documentos: valida pertenencia a solicitud/ámbito, identidad, tipo, MIME, tamaño, checksum y scanStatus. Guarda referencia y checkpoint en una transacción con fencing. Trigger SQL inducido revierte ambos; recuperación READ_ONLY repite consulta y completa una única referencia. Servicio documental mock, sin subir/descargar archivos ni ejecutar antivirus local.
+- Configuración: wiring Spring del registro comprobado con worker activo y administración apagada; catálogos vacíos predeterminados no autorizan endpoints. Ejemplos cargados por loaders reales; URLs .invalid, sin credenciales. Compose conserva un servicio, montaje config de solo lectura y PostgreSQL externo. No se requieren nuevas migraciones; V1/V2/V3 intactas.
+- Alcance: A01/A02 y separación técnica de A03 cubiertos en fixtures; retries/backoff/Retry-After de A03 pendientes T12 y recorrido completo T14/T16. Integración real de clientes/documentos/buró requiere contratos/credenciales; no se contactó proveedor real, Keycloak ni runtime orquestador/server02:8787. Docker no ejecutado.
+- Regresión completa final: `mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`, JDK 21.0.12.1; BUILD SUCCESS, 13:54 min, finalizado 2026-10-27T11:40:22-06:00. 301 pruebas Surefire + 99 Failsafe = 400; cero fallos/errores/omisiones. IT: migraciones 40, eventos 10, publicación 11, HTTP T11 6, solicitudes 9, seguridad 6 y worker 17. Reportes en target/surefire-reports y target/failsafe-reports.
+- Aislamiento: credenciales MIGRATION_TEST_DB_* solo en entorno de proceso; exclusivamente orquestador_test. Schemas temporales t03/t06/t07 y roles runtime propios limpiados por AfterAll. La regresión usó t03_f14d09faa2f443e5929e85f05eec3e6d y fixture compartido t07_e677f961a04a4e83b6814d6f21c4a72b, recreado/limpiado por cada suite correspondiente. No se modificó la base runtime.
+- Artefacto: target/orquestador-nmp-0.1.0-SNAPSHOT.jar, SHA256 2F9CC8EEB70FB7B966276BD98445B94732A9E70EEBB6424A105B18571646F0A0. Inspección ZIP confirma ausencia de mocks/clases de prueba; git diff --check aprobado.
+- T11 DONE; siguiente T12 PENDING con dependencia DONE. Ninguna tarea principal IN_PROGRESS. Proveedores reales y despliegue siguen pendientes, no se presentan como pruebas aprobadas.
+
+## E21 — Cierre T12 — 2026-10-27 16:22 (America/Mexico_City)
+
+- Criterios previos: máximo publicado de tres intentos totales por acción; backoff/jitter/Retry-After acotados; RETRY_WAIT durable y atómico; conservar checkpoints y clasificar leases vencidos por efecto; no liberar UNKNOWN; resolve protegido, idempotente, con evidencia y sin elegir destino; fault injection y concurrencia con PostgreSQL real.
+- Archivos: ActionResult/RetryDelay, WorkerStore/PostgresWorkerStore/WorkerEngine, HttpIntegrationHandler; Resolutions/ResolutionStore/PostgresResolutionStore/ResolutionConfiguration/ResolutionController y ApiExceptionHandler. Pruebas RetryPolicyTest/RetryReconciliationIT/CrashWorkerProcess y ampliaciones WorkerIT/HttpProviderFixture/HttpIntegrationIT; fixtures SecurityHttpIT/RequestCreationIT. Contrato OpenAPI, RECONCILIATION.md, ejemplos HTTP, README, WORKER/INTEGRATIONS, ADR-016 y seguimiento actualizados.
+- Primera corrida dirigida: `mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external -Dit.test=RetryReconciliationIT -Dtest=RetryPolicyTest,HttpIntegrationTest`; BUILD SUCCESS, 1:41 min, finalizado 2026-10-27T16:12:00-06:00. 25 unitarias + 8 IT, cero fallos/errores/omisiones. Se amplió después la cobertura de límites, concurrencia y fronteras de caída; incluida en la regresión final.
+- Retry: estado de intento, próxima fecha con reloj DB y RETRY_SCHEDULED comparten transacción; fencing impide reencolar con token obsoleto. READ_ONLY/IDEMPOTENT_WRITE transitorios elegibles; claves/hash se conservan; éxitos previos no se repiten. Agotamiento sin efectos termina FAILED; escrituras inciertas requieren conciliación incluso tras agotar intentos.
+- Resolve: requiere administración habilitada, EXECUTION_OPERATOR y ámbito permitido/coincidente; operador puede diferir del propietario. RESUME valida contrato/versiones/efecto y cobertura de acciones pendientes; CLOSE exige ausencia/compensación declarada con evidencia. Rechaza ausencia frente a escritura SUCCEEDED durable. No se inyectan referencias documentales ni estados destino.
+- Atomicidad/replay: ledger append-only, checkpoints, cola y auditoría se confirman juntos. Mismo sujeto/comando/clave devuelve respuesta original tras completar; cambios de comando/sujeto confligen. Bloqueo solicitud→ejecución serializa resoluciones; fencing aumenta. La evidencia es declaración verificada por operador, no llamada automatizada al proveedor.
+- Caídas: JVM hija exclusiva de prueba termina con Runtime.halt en fronteras anteriores/posteriores al HTTP y con checkpoint previo. El proceso padre reconstruye store/worker usando la DB durable; mock deduplica efecto lógico por clave. No es una prueba de reinicio de contenedor ni idempotencia de un proveedor real.
+- Sin migraciones nuevas: V1/V2/V3 ya contienen estados/ledger/permisos. Compose permanece como un único servicio con PostgreSQL externo; flags por defecto sin cambios. Pruebas únicamente en orquestador_test, sin desplegar ni tocar runtime orquestador/server02:8787 ni proveedores reales.
+- Regresión final: `mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`, JDK 21.0.12.1 y credenciales MIGRATION_TEST_DB_* solo en el proceso. BUILD SUCCESS, 8:25 min, finalizado 2026-10-27T16:22:45-06:00. 303 unitarias Surefire + 116 IT Failsafe = 419, cero fallos/errores/omisiones. IT: migraciones 40, eventos 10, publicación 11, HTTP T11 6, solicitudes 9, RetryReconciliationIT 17, seguridad 6 y worker 17. Reportes en target/surefire-reports y target/failsafe-reports.
+- Evidencia T12: Retry-After 429 real de mock programa 120 s, luego éxito con clave estable; tres fallos READ_ONLY agotan en FAILED sin outcome negativo; escritura idempotente agotada no libera solicitud. Éxitos previos no se repiten. Rollback inducido en auditoría de retry y de resolución conserva checkpoint/cola/ledger anteriores. Replay tras SUCCEEDED conserva respuesta, dos claves concurrentes producen 200/409 y una clave compartida produce una resolución. Datos/estado/revisión de negocio se conservan al cerrar.
+- Cinco caídas reales de JVM: READ_ONLY antes de envío; IDEMPOTENT_WRITE después de HTTP y después de un checkpoint previo; NON_IDEMPOTENT_WRITE antes/después de HTTP. Los seguros recuperan; los no seguros exigen conciliación sin repetir llamada. Checkpoint previo conserva attempt_count=1; mock registra un efecto lógico por clave. WorkerIT confirma token obsoleto rechazado también al programar retry y mantiene pruebas de rollback final (A19).
+- Aislamiento: esquemas temporales propios y roles LOGIN eliminados por AfterAll. Suite final utilizó t03_fa507cbe77934497a335700d620079c6 y fixture compartido t07_6687499ffd2b4582963ebdaccc2425c8 (recreado/limpiado por suites), además del schema propio T06. No se tocó runtime ni se desplegó; Docker y proveedores reales no ejecutados.
+- Artefacto: target/orquestador-nmp-0.1.0-SNAPSHOT.jar, SHA256 7781CC0F17909D712E6FF34B262D2AEF52C34CF65955A2B938A7FF69A91ACC59. ZIP sin CrashWorkerProcess, mocks o clases de prueba. git diff --check aprobado.
+- T12 DONE; siguiente T13 PENDING con dependencias DONE. Ninguna tarea principal IN_PROGRESS. A03 y A12–A14/A19 cubiertos localmente con los límites de proveedor mock indicados; T16 repetirá la matriz integral.
+
+## E22 — Cierre T13 — 2026-10-27 16:50 (America/Mexico_City)
+
+- Alcance confirmado con usuario: backend exclusivamente, sin desarrollar una interfaz gráfica. Criterios previos: consulta autorizada de eventos/schema/ui y bloqueos, versión fijada/retirada, guardias sin datos inventados, documentos ajenos sin vincular, INVALID conserva tarea/motivo, fallo técnico no produce INVALID y payload con resultados de proveedor rechazado (A11/A20).
+- Archivos: AvailableEvents/View/Policy/AvailabilitySnapshot/AvailabilityStore/PostgresAvailabilityStore/AvailableEventsController y RequestConfiguration; document/domain/DocumentMetadata, RegisterDocumentHandler, WorkerEngine. AvailableEventPolicyTest/AvailableEventsIT/DocumentTasksIT, catálogo documental de ejemplo, OpenAPI, AVAILABLE_EVENTS.md, ejemplos HTTP, README, ADR-017 y seguimiento.
+- Verificación dirigida final: `mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external -Dit.test=AvailableEventsIT,DocumentTasksIT -Dtest=AvailableEventPolicyTest,RegisterDocumentTest,DomainArchitectureTest`; BUILD SUCCESS, 2:46 min, finalizado 2026-10-27T16:40:04-06:00. 16 unitarias + 20 IT (8 disponibilidad y 12 documentos), cero fallos/errores/omisiones. Después se añadió el caso de referencia ya vinculada a otra solicitud; incluido en regresión completa.
+- Correcciones de fixtures: publicación rechazó el flujo documental inicial. Se separó documentId de entrada de registeredDocumentId verificado y se declararon tipos string explícitos en enums status/scanStatus del catálogo de ejemplo para el análisis estático. No se relajaron las reglas de datos protegidos ni cambiaron valores aceptados del contrato. Un test puro comprueba la validez del fixture.
+- Disponibilidad: usa una sentencia SQL para revisión/estado/definición fijada/ejecución activa coherentes; una escritura no confirmada no mezcla versiones de esos datos. Conserva ui anterior tras activar v2 y retirar v1. Filtra propietario/ámbito/roles; FLOW_ADMIN no accede a recurso ajeno. FORM y DOCUMENT_UPLOAD se ajustan al schema público. Los cuatro estados activos devuelven events vacío y blockedByExecutionId.
+- Guardias: falso/error conocidos se proyectan como códigos genéricos; cualquier dependencia de event.data se difiere al POST. Prueba HTTP demuestra que un evento anunciado se rechaza con agree=false y se acepta con agree=true. No-store, sin ETag en esta ruta porque la disponibilidad puede cambiar sin revisión nueva; revision sigue disponible para If-Match.
+- Documentos: verificador puro centraliza pertenencia/tipo/MIME/tamaño/checksum/CLEAN. Worker añade límites más restrictivos de la tarea publicada antes de insertar referencia y al reutilizar checkpoint; tamaño 1001 frente a tarea 1000 y MIME PNG permitido por catálogo pero no por tarea se rechazan. Conserva transacción/fencing de referencia+checkpoint T11.
+- Negocio: recorrido real API/worker/HTTP mock/SQL INVALID deja SUCCEEDED/DOCUMENTO_INVALIDO con razón EXPIRED en datos y tarea visible; nuevo evento VALID avanza y oculta tarea sin duplicar referencia. Error 503 reintenta sin repetir metadata exitosa, bloquea tareas durante RETRY_WAIT y al agotar queda FAILED sin fabricar INVALID. Payloads con score/validation/MIME/scanStatus/identidad no crean ejecución.
+- Límites: no se reciben archivos, no se calcula MIME/checksum desde bytes ni se ejecuta antivirus; se verifica el dictamen del servicio documental confiable. Solo mocks de prueba y datos sintéticos. Sin nuevas migraciones, despliegue o cambios al Compose de un servicio con PostgreSQL externo. No se toca runtime orquestador/server02:8787.
+- Regresión completa final: `mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`, JDK 21.0.12.1; BUILD SUCCESS, 9:53 min, finalizado 2026-10-27T16:50:46-06:00. 308 unitarias Surefire + 137 IT Failsafe = 445; cero fallos/errores/omisiones. IT: disponibilidad 8, migraciones 40, documentos 13, eventos 10, publicación 11, HTTP 6, solicitudes 9, retries/conciliación 17, seguridad 6 y worker 17. Reportes en target/surefire-reports y target/failsafe-reports.
+- Caso adicional aprobado: una referencia ya vinculada a solicitud A no se reasigna a B aunque el mock entregue metadatos contradictorios; la referencia original permanece, B no obtiene vínculo, su checkpoint falla y no se invoca el validador posterior.
+- Aislamiento: solo orquestador_test; credenciales MIGRATION_TEST_DB_* en entorno de proceso. Fixtures temporales t07_f89ebc09f5764cc397a280dff4436f93 (recreado/limpiado por suites), t03_0ba9b05d7ca24ad6839dd222fb795062 y schema propio T06; roles/schemas propios limpiados por AfterAll. Sin despliegue, Docker ni proveedor/antivirus real.
+- Artefacto target/orquestador-nmp-0.1.0-SNAPSHOT.jar: SHA256 FD5B3A7718745754910BBB9A81C2338F0CEF7A4577590C5A2334C9DCFDB0AA8E. ZIP sin mocks, suites ni proceso de caída de prueba. git diff --check aprobado.
+- T13 DONE; siguiente T14 PENDING con dependencias DONE. Ninguna tarea principal IN_PROGRESS. A11/A20 cubiertos con servicio documental mock; integración real pendiente de contrato/configuración.
+
+## E23 — Cierre T14 — 2026-10-28 (America/Mexico_City)
+
+- Criterios: tres JSON completos publicables, recorridos persistentes por API/worker/SQL,
+ crédito v1/v2 coexistente (A10), licencia por configuración (A18), compra y fronteras
+ 71/70/61/60. Solo backend, proveedores HTTP simulados y datos sintéticos.
+- Archivos: examples/flows/*.json y README/seed; examples/integrations/catalog y rutas;
+ examples/http/demo-flows.http; DemoFlowTest/DemoFlowsIT; README y ADR-018.
+- Validación inicial: DemoFlowTest, 3 casos aprobados (2026-10-27 23:51).
+- Primera integración: 9 de 10 casos aprobados; la comprobación SQL usaba el nombre
+ flow_version_id en vez de flow_id. Corregido exclusivamente en test.
+- Segunda integración: 8 de 10 casos aprobados; segunda licencia usaba otro UUID con
+ el mismo storageDocumentId sintético, infringiendo unicidad. Corregido el fixture
+ para identificar cada documento. Un caso quedó RETRY_WAIT; se amplió el plazo HTTP
+ del fixture demo de conexión 100 ms/respuesta 2 s a 1 s/5 s. Producción sin cambios.
+- Hash agregado de src/main antes/después: 203974E7294C38BE32A19B44C51A6EDD371081B76A355A6688F59DDF32143B49.
+ Igualdad verificada: no cambios Java, recursos o migraciones de producción en T14.
+- Pendiente resultado de verificación final: todas las unitarias + DemoFlowsIT,
+ DocumentTasksIT y HttpIntegrationIT. Se conserva E22 como regresión completa base;
+ no se atribuirá esa corrida anterior a T14.
+- Alcance pendiente independiente: /admin/flows/{id}/simulate no implementado; revisar
+ en T16 antes de entrega T17 (ADR-018). La demo actual usa API operativa con mocks.
+- DemoFlowsIT final: 10/10 aprobados, 166.8 s. Publicación/activación HTTP de los tres
+ archivos exactos; ambos créditos terminan después de activar v2, con flow_id distinto.
+ Rechaza buró prematuro en v2, conserva EXPIRED/LICENCIA_PENDIENTE y permite reemplazo
+ válido. Compra confirma/cancela sin invocar documentos/buró. Score ausente/textual
+ termina FAILED y conserva BURO_PENDIENTE; no produce REVISION_MANUAL.
+- Surefire final: 311/311 aprobados. JAR sin suites/mocks/proceso de caída; SHA256
+ FD5B3A7718745754910BBB9A81C2338F0CEF7A4577590C5A2334C9DCFDB0AA8E,
+ idéntico al artefacto T13. Pendiente únicamente finalizar regresión documental/HTTP.
+- Verificación final completada: `mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external -Dit.test=DemoFlowsIT,DocumentTasksIT,HttpIntegrationIT`.
+ BUILD SUCCESS, 5:53 min, finalizado 2026-10-28T01:14:02-06:00. 311 unitarias +
+ 29 IT = 340, cero fallos/errores/omisiones. IT: demo 10, documentos 13, HTTP 6.
+ Esta corrida sustituye los resultados fallidos de desarrollo descritos arriba.
+ No se repitieron las demás IT de E22: src/main y JAR permanecieron idénticos;
+ se verificó la regresión de consumidores del catálogo de ejemplo modificado.
+- Reportes: target/surefire-reports, target/failsafe-reports y target/t14-final.log.
+ JDK 21 local. Solo orquestador_test; schema temporal t07_41df5db3c155418d99cbe30611c405aa
+ y rol propio limpiados por AfterAll (fixture recreado por suite). Credenciales
+ exclusivamente en entorno de proceso; no se tocó runtime ni se desplegó Docker.
+- T14 DONE. T15 PENDING con dependencias DONE; ninguna tarea principal IN_PROGRESS.
+ Los pendientes históricos de los párrafos anteriores quedan resueltos por esta
+ corrida, salvo proveedores reales y endpoint simulate explícitamente indicados.
+
+## E24 — Cierre T15 — 2026-10-28 (America/Mexico_City)
+
+- Criterios previos: historial paginado/filtrado con ACL por recurso, auditoría
+ atómica, métricas sin cardinalidad por solicitud, logs sin PII, readiness/liveness
+ y apagado del worker. Única tarea principal IN_PROGRESS. Sin despliegue.
+- Implementación: audit/api/HistoryController e infrastructure/HistoryStore;
+ QueueMetrics/ObservabilityConfiguration, CommandObservationFilter; seguridad de
+ Actuator; timers HTTP, logs/códigos/razones del worker y presupuesto de drenaje.
+ OBSERVABILITY.md, ADR-019, OpenAPI, ejemplo HTTP, README/RUNBOOK/SECURITY y env/Compose.
+- Primera dirigida: `mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external -Dtest=ObservabilityTest,DomainArchitectureTest,HttpIntegrationTest -Dit.test=ObservabilityIT`.
+ BUILD SUCCESS, 1:54 min, finalizado 2026-10-28T01:47:22-06:00; 27 unitarias y
+ 15 IT, cero fallos/errores/omisiones. Apagado: latches prueban renovación durante
+ drenaje, interrupción por presupuesto y ausencia de nuevas reclamaciones.
+ Historial: empates de timestamp, filtros, binding de cursor, ACL, redacción y
+ triggers append-only; gauges SQL/DB-time y acceso a Actuator. Pool propio cerrado
+ como fallo controlado: readiness 503/liveness 200. No es caída de DB productiva.
+- Se añadió después captura de logs con cliente y score centinela, y comprobación
+ durable de reasonCodes en DocumentTasksIT. Están incluidas en regresión final.
+- Regresión completa en curso: `mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`.
+ 314 unitarias ya aprobadas; IT pendientes de terminar. No marcar DONE hasta
+ resultado final. Usa exclusivamente orquestador_test y esquemas/roles temporales;
+ secretos solo en entorno del proceso. Proveedores HTTP mock y datos sintéticos.
+- Regresión completa: ObservabilityIT 16/16 aprobados, incluida captura de logs;
+ DocumentTasksIT 13/13 aprueba reasonCodes en auditoría, y DemoFlowsIT 10/10.
+- Revisión final detectó binding ambiguo entre kind omitido y literal "null".
+ Se distingue ausencia con cadena vacía (inválida como kind) y se agrega aserción
+ HTTP específica. La corrida completa ya había compilado la versión previa;
+ al terminar se ejecutará nuevamente el caso de cursor y tests de operación/
+ arquitectura contra el código final. No se atribuye ese ajuste a la corrida base.
+- Regresión completa terminada: BUILD SUCCESS, 12:45 min, finalizado
+ 2026-10-28T02:01:05-06:00. Surefire 314 + Failsafe 163 = 477; cero
+ fallos/errores/omisiones. IT: disponibilidad 8, migraciones 40, demo 10,
+ documentos 13, eventos 10, publicación 11, HTTP 6, observabilidad 16,
+ solicitudes 9, reintentos/conciliación 17, seguridad 6 y worker 17.
+- WorkerIT conserva 17 casos aprobados: fallo inducido de inserción auditada hace
+ rollback de estado/revisión; recuperación reutiliza checkpoint exitoso, reglas
+ y actualización se confirman junto con auditoría, renovación y fencing vigentes.
+- Reportes de la corrida completa conservados en target/t15-full-reports.zip antes
+ del check final dirigido del cursor. Log completo t15-final.log (ignorado por git).
+- Check del código final: `mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external -Dtest=ObservabilityTest,DomainArchitectureTest -Dit.test=ObservabilityIT#historyAuthorizationAndCursorBindingNeverExposeForeignResource`.
+ BUILD SUCCESS, 59.678 s, finalizado 2026-10-28T02:02:47-06:00; 4 unitarias +
+ 1 IT, sin fallos/errores/omisiones. Cursor con kind omitido vs literal null
+ ya no comparte binding; también rechaza otro filtro y otra solicitud, y aplica ACL.
+- Evidencia final: target/t15-cursor-final.log y reportes actuales; corrida completa
+ preservada en target/t15-full-reports.zip y t15-final.log. JAR final
+ target/orquestador-nmp-0.1.0-SNAPSHOT.jar SHA256
+ B71C97EB0D16504069B326088F270F9546E4DD7379B876C4F05AF412B148370E.
+ ZIP sin suites/mocks/proceso de caída. git diff --check aprobado.
+- Sin nuevas migraciones; historial/razones conservan transacción de negocio y
+ permisos append-only. Runtime orquestador/server02:8787 y proveedores reales
+ no usados. Compose sigue con un servicio y PostgreSQL externo; no se desplegó.
+- T15 DONE. Los pendientes de verificación descritos arriba quedan resueltos por
+ las corridas finales. T16 PENDING con dependencias DONE, ninguna tarea IN_PROGRESS.
+ T16 conserva el pendiente explícito de simulate de ADR-018; Docker/integraciones
+ reales y política organizacional de retención no se declaran verificados aquí.
+
+## E25 — T16: implementación verificada, cierre bloqueado por Docker — 2026-10-28 (America/Mexico_City)
+
+- Criterios: matriz A01–A20 ejecutada sin skips, reinicios/concurrencia/rollback/ACL,
+ reportes conservados y CI reproducible con Docker; cerrar /simulate pendiente.
+ Única tarea principal IN_PROGRESS. T17 no iniciado, sin despliegue.
+- Implementación: FlowSimulation/SimulationController; TransitionEvaluator extraído
+ mecánicamente de WorkerEngine para compartir proyección pura. Contrato y fixtures
+ administrativos, sin HTTP ni escrituras. NO_OP solo en simulación. Runtime sigue
+ con contratos fijados, idempotencia, checkpoints, leases y auditoría existentes.
+- Pruebas nuevas: FlowSimulationTest (12 casos), SimulationIT (2), DemoFlowsIT
+ agrega API cliente 503 con agotamiento y replay antes/después de ejecutar con
+ una llamada al proveedor; cliente ausente comprueba outcome explícito.
+- Matriz ejecutable specs/acceptance-cases.json y scripts/verify_acceptance.py.
+ Rechaza métodos ausentes, fallos/errores/skips; distingue external-postgresql de
+ testcontainers según XML real. 2 tests Python del checker aprobados. CI exige
+ docker info, clean verify y gate A01–A20; artefactos por 30 días. Maven falla si
+ no encuentra tests. No se afirma ejecución del workflow remoto.
+- Verificación dirigida: `mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external -Dtest=FlowSimulationTest,DomainArchitectureTest -Dit.test=SimulationIT`.
+ BUILD SUCCESS, 1:00 min, finalizado 2026-10-28T06:27:09-06:00. 13 unitarias +
+ 2 IT, cero fallos/errores/omisiones. El test de referencia externa se alineó al
+ rechazo estructural previo del DSL, sin relajar el filtro ni resolver la URL.
+- Docker: Get-Command docker no encuentra ejecutable; no servicio Docker ni archivo
+ en instalación estándar; DOCKER_HOST/TLS/CERT_PATH/HOST_OVERRIDE sin configurar.
+ Se solicitó motor autorizado al usuario. Pendiente prueba directa Testcontainers.
+- Regresión completa en curso con JDK 21 y perfil migration-tests-external,
+ exclusivamente orquestador_test. Hasta ahora 326 unitarias, 40 migraciones y
+ 12 recorridos demo aprobados. Proveedores/JWKS mock; secretos solo en proceso.
+- Archivos operativos: SIMULATION.md, ACCEPTANCE.md, ADR-020, README, OpenAPI,
+ examples/http/simulate.http, .github/workflows/ci.yaml, pom.xml y seguimiento.
+- Regresión completa externa: `mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external`.
+ BUILD SUCCESS, 18:22 min, finalizado 2026-10-28T06:48:10-06:00. 326 unitarias +
+ 167 IT = 493; cero fallos/errores/omisiones. IT: disponibilidad 8, migraciones 40,
+ demo 12, documentos 13, eventos 10, publicación 11, HTTP 6, observabilidad 16,
+ solicitudes 9, retry/conciliación 17, seguridad 6, simulación 2 y worker 17.
+- `python scripts/verify_acceptance.py`: passed=true, environment=external-postgresql,
+ todos los métodos/parametrizaciones de A01–A20 ejecutados y aprobados. Dos tests
+ Python del checker también aprobados; incluye rechazo de un skip no mapeado.
+- Puerta Docker con esos mismos reportes: `python scripts/verify_acceptance.py --require-testcontainers --output target/acceptance-docker-gate.json` devuelve passed=false
+ por entorno externo. Es el resultado esperado: no atribuir evidencia Testcontainers
+ a esta corrida. Reportes completos conservados ANTES de la prueba Docker en
+ target/t16-external-reports.zip; JSON target/acceptance-report.json y log t16-final.log.
+- JAR comprobado: SHA256 FA50938E56D5ACCDE505E2FEEB4F76F1F9011BA01B90895084101350C9D16DAC.
+ Prueba directa de detección de Docker en curso con perfil predeterminado.
+- Prueba Docker directa: `mvnw.cmd --batch-mode --no-transfer-progress verify -Dtest=FlowSimulationTest -Dit.test=SimulationIT` (sin perfil externo).
+ BUILD FAILURE, 9.795 s, finalizado 2026-10-28T06:49:37-06:00. Falla setup de
+ SimulationIT con `Could not find a valid Docker environment`; 1 error de entorno,
+ sin skips. No inicia PostgreSQL ni invoca runtime externo. La suite externa
+ anterior conserva su evidencia aprobada en ZIP/JSON. Los reportes XML actuales
+ contienen la prueba negativa de Docker y no deben confundirse con aquel archivo.
+- Bloqueo externo confirmado, no defecto funcional ni prueba Docker aprobada.
+ T16 BLOCKED. No se marca DONE ni se inicia T17. Para cerrar: motor Docker local
+ o remoto autorizado; `mvnw.cmd --batch-mode --no-transfer-progress clean verify`
+ y `python scripts/verify_acceptance.py --require-testcontainers`, sin skips.
+- Sin migraciones nuevas, cambios al Compose de un servicio, despliegue productivo
+ o llamadas a proveedores reales. Conservadas auditoría/idempotencia/versiones;
+ extracción del evaluador respaldada por regresión completa. git diff --check OK.
diff --git a/specs/RECONCILIATION.md b/specs/RECONCILIATION.md
new file mode 100644
index 0000000..347a21e
--- /dev/null
+++ b/specs/RECONCILIATION.md
@@ -0,0 +1,77 @@
+# Reintentos y conciliación — T12
+
+## Reintentos durables
+
+Los fallos TRANSIENT de READ_ONLY e IDEMPOTENT_WRITE se reencolan en RETRY_WAIT.
+La programación, el fallo del intento y RETRY_SCHEDULED se confirman juntos, con
+fencing y tiempo de PostgreSQL. No hay un sleep del worker ni una cola en memoria.
+La solicitud permanece ocupada y conserva estado/datos de negocio.
+
+La política publicada limita a tres intentos totales por acción (incluido el
+primero). Defaults: base exponencial 2 s/4 s, jitter entre 80 % y 120 %, limitado
+por maxBackoff. Se respeta Retry-After numérico o fecha HTTP, acotado a 120 s;
+valores inválidos se ignoran. El mayor de backoff con jitter y Retry-After define
+el retraso. Los checkpoints SUCCEEDED no se repiten y la clave externa
+executionId:actionId permanece estable.
+
+Al agotar intentos READ_ONLY sin escrituras pendientes se termina FAILED.
+Escrituras inciertas, incluyendo una IDEMPOTENT_WRITE agotada, mantienen
+RECONCILIATION_REQUIRED. Una operación NON_IDEMPOTENT_WRITE RUNNING al perder
+lease jamás se repite automáticamente; incluso una caída antes del envío exige
+conciliación si no existe prueba durable de ausencia de efecto. El límite de
+intentos no puede liberar esa incertidumbre.
+
+## Resolución por operador
+
+POST /api/v1/admin/executions/{id}/resolve requiere:
+
+- FLOW_ADMIN_ENABLED=true y runtime DB restringido.
+- JWT válido, rol EXECUTION_OPERATOR, ámbito en ADMIN_BUSINESS_SCOPES y coincidente
+ con la solicitud. El operador puede diferir de su propietario.
+- Idempotency-Key obligatorio y comando ReconciliationCommand del contrato.
+- Ejecución RECONCILIATION_REQUIRED para una resolución nueva.
+
+RESUME registra resultados verificados de acciones existentes y reencola PENDING.
+La salida debe cumplir el contrato/perfil fijado y el efecto del checkpoint debe
+coincidir con el catálogo confiable. No se permiten acciones inventadas, IDs
+duplicados, incertidumbres pendientes ni sobrescribir un checkpoint exitoso con
+otro resultado. Una acción ya exitosa puede incluirse únicamente con el mismo
+resultado; no vuelve a ejecutarse. No se inyectan resultados manuales de
+REGISTER_DOCUMENT: sus referencias deben provenir de la verificación documental.
+La solicitud debe conservar estado y revisión aceptados.
+
+CLOSE_WITHOUT_TRANSITION exige ABSENT_VERIFIED o COMPENSATED_VERIFIED y cierra
+FAILED sin transición, cambios de datos ni incremento de revisión. ABSENT_VERIFIED
+se rechaza cuando existe una escritura SUCCEEDED durable; en ese caso corresponde
+documentar compensación. La solicitud vuelve a admitir eventos.
+
+La evidencia exige reference y reason no vacíos. **La verificación la realiza el
+operador fuera del servicio**: estos campos son una declaración autorizada y
+auditada, no una consulta automática al proveedor. Usar una referencia a un caso
+verificable; no adjuntar documentos personales, JWT o secretos.
+
+Resolución, checkpoints, cola y EXECUTION_RESOLVED comparten transacción. El ledger
+es append-only. Idempotencia por executionId/clave; replay exige mismo comando
+canónico y mismo sujeto operador, y conserva respuesta original incluso tras
+completar la ejecución. Otro comando u operador con esa clave recibe 409.
+Dos operadores con claves distintas no pueden aplicar simultáneamente dos
+resoluciones nuevas. La resolución incrementa fencing_token.
+
+No existe campo para elegir un estado destino. El worker continúa aplicando la
+definición publicada y confirma la transición de forma normal.
+
+## Pruebas
+
+RetryPolicyTest cubre límites y parsing. RetryReconciliationIT usa HTTP/JWT,
+PostgreSQL real y triggers de fallos de auditoría. CrashWorkerProcess es una JVM
+exclusiva de test que termina con Runtime.halt antes/después de llamar a un mock
+HTTP: el proceso padre reconstruye el worker y recupera desde DB. El mock registra
+un efecto lógico por clave; no acredita idempotencia de un proveedor real.
+
+WorkerIT conserva las pruebas de lease vencido, worker obsoleto, fallo después del
+checkpoint y rollback de confirmación final. HttpIntegrationIT ahora espera
+RETRY_WAIT en timeout/503 y FAILED en salida inválida.
+
+Ejecutar mvnw.cmd clean verify -Pmigration-tests-external con MIGRATION_TEST_DB_*
+en el proceso y base orquestador_test. Sin ese perfil se requiere Docker para
+Testcontainers. No se despliega ni se utiliza un proveedor real.
diff --git a/specs/REQUESTS.md b/specs/REQUESTS.md
new file mode 100644
index 0000000..e51b431
--- /dev/null
+++ b/specs/REQUESTS.md
@@ -0,0 +1,88 @@
+# Creación y consulta de solicitudes — T08
+
+Implementados `POST /api/v1/requests` y `GET /api/v1/requests/{id}` según el contrato
+OpenAPI. No se implementan todavía eventos, worker, historial ni tareas del front.
+
+## Habilitación
+
+`REQUEST_API_ENABLED=true` habilita ambas rutas y exige un runtime PostgreSQL
+restringido con credenciales de migración separadas (ver SECURITY.md). Es
+independiente de `FLOW_ADMIN_ENABLED`: se puede atender solicitudes sin exponer
+administración ni cargar un catálogo de publicación local. Ambos interruptores
+son false por defecto para conservar el despliegue previo con pgadmin hasta que
+se configure la cuenta de ejecución correcta. No se modificaron cuentas remotas.
+
+El JWT debe contener sub, audiencia, firma y ámbito válidos. Cualquier actor así
+autenticado puede crear una solicitud de un tipo habilitado; propietario y ámbito
+se obtienen del token. Los roles de transición serán exigidos al aceptar eventos
+en T09. No se introduce un rol creador nuevo ni una ACL de tipos inexistente.
+
+## Contrato
+
+POST recibe exclusivamente `typeKey` y `data`, con `Idempotency-Key` obligatorio
+(1–200 caracteres ASCII visibles, sin espacios; una sola cabecera). Retorna 201,
+Location y ETag fuerte. UUID, estado inicial, flujo, propietario, ámbito y revisión
+son asignados por el servidor. Los datos deben satisfacer initialDataSchema y
+dataSchema de la versión publicada; si falta initialDataSchema se usa dataSchema.
+No se aplican defaults ni conversiones implícitas. Los schemas cerrados rechazados
+en publicación protegen campos provenientes de proveedores.
+
+Límite de datos: 256 KiB de JSON canónico UTF-8; cuerpo: 256 KiB + 4096 bytes,
+leído de forma acotada también sin Content-Length. Profundidad JSON máxima 64.
+Se rechazan claves duplicadas, JSON adicional, Unicode incompatible con JSONB,
+campos adicionales de comando y números fuera de los límites del codec T06.
+Tamaño excesivo retorna 413; sintaxis/comando inválido 400; datos fuera del schema
+422. Tipo inexistente: 404. Sin flujo publicado activo, deshabilitado o cambiado
+concurrentemente durante la asignación: 409 INVALID_FLOW_STATE. Un comando fallido
+no reserva su clave y puede reintentarse tras corregir la causa.
+
+GET retorna versión fijada, estado, datos, revisión, fechas y activeExecutionId
+si existe ejecución PENDING/RUNNING/RETRY_WAIT/RECONCILIATION_REQUIRED. La lectura
+SQL obtiene instancia/flujo/ejecución activa en un solo snapshot. Exige propietario
+y ámbito, incluso para FLOW_ADMIN; inexistente y ajeno retornan 404. El DTO omite
+owner/scope y los campos opcionales ausentes. La creación no copia la versión de
+flujo a request_instance: la consulta la obtiene de flow_id.
+
+## Idempotencia y transacción
+
+Identidad de la clave: actor + ámbito + Idempotency-Key. Hash SHA-256 del comando
+canónico typeKey/data usando el codec T06: orden de propiedades y escala decimal
+equivalente no cambian el hash. Otra identidad tiene un espacio de claves distinto.
+
+El repositorio usa READ COMMITTED y la misma DataSource para RequestStore y
+FlowStore. Antes de consultar idempotencia toma pg_advisory_xact_lock con 64 bits
+de SHA-256 sobre schema/actor/ámbito/clave encuadrados por longitud. Una colisión
+solo serializa claves distintas; la búsqueda y restricción única SQL siguen usando
+la identidad completa. No hay locks de proceso ni caché de respuestas en memoria.
+
+Si existe registro y coincide el hash, se verifica acceso actual a la solicitud y
+se devuelve el snapshot original de creación (201, Location y ETag originales),
+aunque el flujo se haya retirado, el tipo esté deshabilitado o la solicitud haya
+avanzado. GET obtiene el estado actual. Un hash distinto retorna 409
+IDEMPOTENCY_CONFLICT antes de resolver de nuevo el flujo activo.
+
+Sin registro previo, la puerta T06 bloquea flujo y tipo en ese orden; luego valida
+datos, inserta solicitud con revisión 0, REQUEST_CREATED e idempotencia con respuesta
+original. Todo confirma o revierte junto. La auditoría incluye correlationId y
+referencias, sin duplicar datos personales. No se invocan servicios externos.
+Un cambio de activo detectado al tomar los bloqueos produce conflicto reintentable;
+no se asigna silenciosamente el flujo leído antes de la carrera. Un retiro espera
+una creación que ya tiene sus locks; una vez confirmado impide nuevas asignaciones.
+
+## Verificación
+
+`mvnw.cmd clean verify -Pmigration-tests-external`, con MIGRATION_TEST_DB_* solo en
+entorno, ejecuta la suite contra orquestador_test. RequestCreationIT reutiliza el
+fixture aislado T07 (schema t07_UUID, LOGIN temporal y JWT RSA/JWKS local) con
+administración apagada y API de solicitudes encendida. No se prueba una emisión
+real de Keycloak ni se despliega el servicio en server02:8787.
+
+Casos: contrato RequestView/Problem, replay y conflicto, versiones fijadas, retiro,
+separación de claves por actor/ámbito, acceso ajeno, validación/tamaño, concurrencia
+misma clave y comandos distintos, activación y retiro con barreras, ejecución activa,
+y rollback SQL real en auditoría/registro idempotente. RequestJsonTest verifica
+parseo estricto, canonicalización decimal y límites UTF-8.
+
+Desde T09 se implementa también POST de eventos (EVENTS.md). La referencia anterior
+al carácter pendiente de eventos describe el alcance de T08; worker, tareas e
+historial siguen pendientes. REQUEST_API_ENABLED habilita ambos módulos.
diff --git a/specs/RUNBOOK.md b/specs/RUNBOOK.md
index dc83342..843653b 100644
--- a/specs/RUNBOOK.md
+++ b/specs/RUNBOOK.md
@@ -51,7 +51,7 @@ docker compose stop # Detener el servicio
docker compose down # Eliminar contenedor y red de Compose
```
-Estos comandos no administran la base externa. Flyway permanece habilitado y aplicará las migraciones incluidas al arrancar; actualmente no existen migraciones SQL de negocio. La base debe existir antes del arranque. La separación de roles de migración/ejecución queda pendiente de T03/T07.
+Estos comandos no administran la base externa. Flyway permanece habilitado y aplicará las migraciones incluidas al arrancar; se incluyen V1/V2 del modelo T03. La base debe existir antes del arranque. Las credenciales de migración pueden separarse según las instrucciones al final de este documento.
## Ejecución desde el IDE
@@ -94,3 +94,94 @@ Son cinco pruebas de sólo lectura; resultados en `target/failsafe-reports`. El
- Fallo de build: comprobar acceso a los repositorios de dependencias.
Registrar evidencia en PROGRESS. No considerar verificado el arranque si no se ejecutó contra Docker y PostgreSQL externo.
+
+## Pruebas T03 sobre PostgreSQL externo
+
+Con JDK 21, sin Docker local, usar exclusivamente la base aislada creada para
+pruebas. Las credenciales se suministran como variables de proceso:
+
+```powershell
+$env:MIGRATION_TEST_DB_URL='jdbc:postgresql://server02:30432/orquestador_test'
+$env:MIGRATION_TEST_DB_USER='pgadmin'
+# Establecer MIGRATION_TEST_DB_PASSWORD mediante el entorno local.
+.\mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pmigration-tests-external
+```
+
+El perfil no utiliza DB_URL y rechaza cualquier base distinta de orquestador_test.
+Crea un esquema t03_, ejecuta las migraciones y lo elimina al finalizar.
+No toca otros esquemas. Crea un rol NOLOGIN temporal dentro de una transacción
+revertida. Requiere permiso de crear esquemas y roles. Si el proceso se interrumpe
+abruptamente, un administrador puede revisar el esquema temporal que haya quedado;
+no borrar otros esquemas ni ejecutar Flyway clean.
+
+Sin ese perfil, verify requiere Docker y ejecuta la misma suite con Testcontainers
+postgres:9.6.24. No hay fallback silencioso ni pruebas omitidas sin Docker.
+El perfil deployment-smoke agrega comprobaciones HTTP; postgres96-check agrega
+una comprobación de arranque de solo lectura, que exige migraciones ya aplicadas.
+Ambos conservan los IT normales y pueden combinarse con migration-tests-external.
+
+## Esquema y credenciales de migración
+
+Flyway aplica V1/V2 al esquema orchestrator. DB_MIGRATION_USERNAME y
+DB_MIGRATION_PASSWORD permiten separar migrador de usuario de aplicación; si se
+omiten, se reutilizan las credenciales DB_USERNAME/DB_PASSWORD. Para aplicar
+mínimo privilegio seguir [roles y migraciones](../src/main/resources/db/migration/README.md).
+
+El historial nuevo es orchestrator.flyway_schema_history. El arquetipo anterior
+no incluía SQL de negocio. Si existe historial en public con migraciones reales,
+detener la actualización y preparar su traslado explícito; no usar baseline ni
+repair para ocultarlo. Esta implementación no despliega ni migra automáticamente
+el servicio remoto: las migraciones se ejecutarán cuando se arranque el nuevo JAR.
+
+## Administración interna T06
+
+El JAR incluye el validador y servicios internos de flujos, sin nuevas rutas
+administrativas. Ver [publicación y catálogo](FLOW_PUBLICATION.md) para el alcance,
+roles, canonicalización y orden de bloqueos. T07 incorporará el wiring y acceso
+REST con identidad verificada; T06 no configura contratos reales de proveedores.
+
+El perfil migration-tests-external ejecuta tanto DatabaseMigrationIT como
+FlowPublicationIT. Esta última crea y elimina únicamente su esquema t06_
+en orquestador_test, incluyendo un trigger de fallo de auditoría exclusivo de prueba.
+La configuración de esquema normal del servicio sigue siendo orchestrator.
+
+T07: antes de habilitar FLOW_ADMIN_ENABLED revisar SECURITY.md. Un runtime pgadmin
+será rechazado; provisionar rol restringido y separar las credenciales Flyway.
+ADMIN_BUSINESS_SCOPES vacío deniega administración. JWT_BUSINESS_SCOPE_CLAIM debe
+coincidir con el claim firmado por Keycloak; el valor por defecto es business_scope.
+
+T08: REQUEST_API_ENABLED=true habilita creación/consulta, independientemente de
+FLOW_ADMIN_ENABLED. Se exige rol DB restringido también con administración apagada.
+Provisionar y activar un tipo antes de crear; REQUESTS.md documenta respuestas,
+reintentos y pruebas. Un replay POST trae revisión original; refrescar con GET para
+obtener el estado/ETag actual antes de aceptar futuros eventos.
+
+T09: REQUEST_API_ENABLED habilita también POST de eventos. Conservar Idempotency-Key,
+cuerpo e If-Match originales al reintentar un envío sin respuesta. GET de solicitud
+muestra revisión/ejecución activa. Hasta implementar T10, PENDING no se procesa y GET
+de ejecución no está disponible. Ver EVENTS.md; no invocar endpoints de proveedores.
+
+T10: aplicar V3 con usuario de migraciones antes de habilitar WORKER_ENABLED.
+GET de ejecución se habilita con REQUEST_API_ENABLED. Revisar WORKER.md para
+leases, checkpoints, apagado y recuperación. Dejar worker apagado en flujos con
+integraciones hasta configurar adapters T11. Defaults 60 s lease / 15 s renovación;
+los timeouts externos futuros deben ser menores y acotados. No hay retry HTTP T12 aún.
+
+## Seguimiento T15
+
+Consultar /api/v1/requests/{id}/history como propietario para enlazar correlationId
+con executionId, ruleIds y reasonCodes. Usar nextCursor con los mismos filtros;
+from inclusivo/to exclusivo requieren UTC Z. Consultar OBSERVABILITY.md y
+examples/http/history-operations.http.
+
+El operador autorizado consulta Actuator metrics. Vigilar la edad del pendiente,
+leases expirados y RECONCILIATION_REQUIRED; no liberar ni reintentar ciegamente
+escrituras inciertas. Si queue.sample.available=0, los gauges NaN son desconocidos,
+no una cola vacía. No sumar estados DB de réplicas conectadas a la misma base.
+
+Al detener, Compose concede 40 s; el worker drena por WORKER_SHUTDOWN_SECONDS (1–20),
+renueva los trabajos vigentes y luego interrumpe. Un trabajo incompleto conserva
+lease/checkpoints: recuperar según T12. Readiness depende de DB; liveness no.
+No cambiar el timeout de HTTP a uno mayor que el lease ni presumir cancelación
+externa al interrumpir. Retención/borrado de auditoría requiere política aprobada
+con procedimiento privilegiado separado, sin borrado automático por runtime.
diff --git a/specs/SECURITY.md b/specs/SECURITY.md
new file mode 100644
index 0000000..5e023e5
--- /dev/null
+++ b/specs/SECURITY.md
@@ -0,0 +1,109 @@
+# Seguridad T07
+
+El servicio valida tokens RS256 mediante JWKS, emisor exacto, audiencia configurada,
+expiración obligatoria y sujeto no vacío. También valida nbf cuando está presente;
+el validador temporal de Spring admite 60 segundos de desfase. Las pruebas usan
+tokens RSA realmente firmados y un servidor JWKS local. No acreditan todavía una
+emisión real de Keycloak controlms: faltan un token de prueba y su configuración
+de audiencia/claims, sin necesidad de entregar contraseñas al repositorio.
+
+`JWT_AUDIENCE` identifica la audiencia y el cliente cuyos roles se aceptan desde
+`resource_access`. También se aceptan `realm_access.roles`. Roles de otros clientes
+y el claim OAuth `scope` no conceden permisos técnicos. `JWT_BUSINESS_SCOPE_CLAIM`
+selecciona el claim de negocio (por defecto `business_scope`); debe ser un string
+no vacío. Identidad, roles y ámbito proceden exclusivamente del JWT verificado.
+
+## Administración
+
+`FLOW_ADMIN_ENABLED=false` mantiene deshabilitados los controladores administrativos.
+Para habilitarlos se necesita un usuario DB de ejecución restringido, credenciales
+de migración separadas, catálogo confiable y `ADMIN_BUSINESS_SCOPES` con ámbitos
+permitidos separados por comas. Una lista vacía deniega todas las operaciones.
+Los flujos y tipos son un catálogo global: pertenecer a uno de esos ámbitos autoriza
+administrarlo según el rol; no implica aislamiento de flujos por tenant.
+
+| Operación implementada | Rol requerido |
+|---|---|
+| POST /api/v1/admin/flows; PUT /api/v1/admin/flows/{id} | FLOW_EDITOR |
+| POST /api/v1/admin/flows/{id}/validate | FLOW_EDITOR o FLOW_PUBLISHER |
+| POST /api/v1/admin/flows/{id}/publish | FLOW_PUBLISHER |
+| POST /api/v1/admin/flows/{id}/retire | FLOW_ADMIN |
+| PUT /api/v1/admin/request-types/{key}/active-flow | FLOW_ADMIN |
+
+Edición, publicación, retiro y activación requieren un ETag fuerte `If-Match`.
+Ausente: 428; sintaxis inválida: 400; revisión obsoleta: 412. Editar un flujo publicado
+retorna 409. JSON inválido/duplicado: 400; definición fuera de schema: 422;
+cuerpo superior a 1 MiB: 413. Respuestas de error no exponen SQL ni tokens y llevan
+un correlationId generado por el servidor. Sesiones y caché de peticiones deshabilitadas.
+
+`config/publication-catalog.json` es configuración de despliegue de solo lectura en
+Docker. No puede reemplazarse desde HTTP. El ejemplo registra ANALISTA y ningún
+handler, integración ni documento; no simula proveedores ni permite publicar
+acciones no registradas. Reiniciar aplica una nueva configuración. Simulación y
+resolución siguen pendientes de sus tareas funcionales; la matriz de roles no
+constituye una implementación de esas rutas.
+
+## Recursos de negocio
+
+`ResourceAccess` exige propietario (`sub`) **y** ámbito coincidentes para solicitud,
+ejecución, documento y auditoría. El adapter resuelve los tres últimos contra su
+solicitud padre en PostgreSQL. Recurso inexistente o ajeno retorna el mismo 404;
+FLOW_ADMIN no permite eludir esta política. La conciliación tiene una autorización
+separada: EXECUTION_OPERATOR y mismo ámbito, aunque el propietario sea otro.
+
+T08 integra esta puerta en la consulta de solicitudes y filtra también propietario/ámbito
+en el SELECT que devuelve los datos. PersistenceAccessConfiguration comparte codec,
+FlowStore, ResourceAccess y la verificación DB cuando administración o solicitudes
+estén habilitadas; REQUEST_API_ENABLED es independiente del interruptor administrativo.
+T09/T12/T13/T15 deben reutilizar la puerta antes de devolver datos o ejecutar comandos.
+Las pruebas T07 de ejecución/documento/auditoría usan un controlador exclusivo de test,
+fuera del paquete escaneado y ausente del JAR; sus endpoints funcionales siguen pendientes.
+T08 prueba POST/GET reales de solicitudes (REQUESTS.md). No se acepta identidad en cuerpos.
+
+## PostgreSQL externo
+
+Configurar `DB_USERNAME`/`DB_PASSWORD` para un rol LOGIN sin privilegios elevados,
+sin propiedad ni CREATE en el schema orchestrator, sin pertenencia a roles elevados.
+Configurar `DB_MIGRATION_USERNAME`/`DB_MIGRATION_PASSWORD` para el propietario que
+aplica Flyway. El DBA provisiona ambos; la aplicación no crea roles automáticamente.
+Después de las migraciones, el propietario concede los permisos de V2 mediante:
+
+```sql
+SELECT orchestrator.grant_runtime_access('orchestrator_app'::name);
+```
+
+El nombre es ilustrativo: el rol debe existir previamente. `RuntimeRoleVerifier`
+rechaza al iniciar administración un runtime elevado, propietario, con DDL en el
+schema, capaz de modificar historiales append-only o acceder a flyway_schema_history.
+Esta comprobación no configura permisos de otras bases/schemas: el DBA controla
+también los permisos heredados de PUBLIC, particularmente en PostgreSQL 9.6.
+No cambiar permisos globales del servidor para ejecutar las pruebas.
+
+Prueba reproducible: `mvnw.cmd clean verify -Pmigration-tests-external`, con
+MIGRATION_TEST_DB_URL/USER/PASSWORD en entorno de proceso. Solo admite la base
+orquestador_test. T07 crea schema t07_ y rol LOGIN t07_runtime_, contraseña
+aleatoria y concesiones limitadas; los elimina en AfterAll. Las pruebas se conectan
+con ese rol real, no con SET ROLE ni una simulación de permisos. Sin perfil externo,
+verify requiere Docker y usa PostgreSQL de Testcontainers.
+
+
+T09 integra ResourceAccess en aceptación de eventos y vuelve a filtrar propietario/
+ámbito bajo SELECT FOR UPDATE. allowedRoles del flujo se comprueba para eventos
+nuevos. El replay requiere acceso actual y sujeto original, pero no vuelve a evaluar
+roles/guardias: solo consulta una aceptación anterior. actor_snapshot guarda atributos
+mínimos verificados; nunca JWT. Ver EVENTS.md.
+
+T10: GET de ejecución usa ResourceAccess y repite owner/scope en la consulta SQL;
+no devuelve inputs, identidad snapshot, leases ni respuestas crudas. WORKER_ENABLED
+habilita infraestructura común y RuntimeRoleVerifier incluso con ambas APIs apagadas.
+El worker usa identidad mínima persistida al aceptar, no reutiliza JWT vencidos.
+
+## T15: historial y métricas
+
+El historial de solicitudes exige propietario y businessScope iguales al JWT;
+FLOW_ADMIN/EXECUTION_OPERATOR no obtienen acceso a historial ajeno. La proyección
+no entrega evidencia de conciliación ni resultados completos de proveedores.
+/actuator/metrics y descendientes requieren EXECUTION_OPERATOR y ámbito autorizado
+en ADMIN_BUSINESS_SCOPES, incluso con FLOW_ADMIN_ENABLED=false. Las tres rutas
+health/liveness/readiness permanecen públicas sin detalles. Resto de /actuator/**
+se deniega. Ver OBSERVABILITY.md para etiquetas permitidas, cursor y logs.
diff --git a/specs/SIMULATION.md b/specs/SIMULATION.md
new file mode 100644
index 0000000..740ff78
--- /dev/null
+++ b/specs/SIMULATION.md
@@ -0,0 +1,37 @@
+# Simulación administrativa
+
+POST /api/v1/admin/flows/{id}/simulate requiere FLOW_ADMIN_ENABLED, FLOW_EDITOR
+y un businessScope permitido por ADMIN_BUSINESS_SCOPES. El actor del JSON es un
+fixture para evaluar roles/guardias del flujo; no autentica al llamador. No acepta
+un token dentro del JSON como autoridad. Flujos DRAFT/PUBLISHED/RETIRED pueden
+evaluarse; nunca se cambia estado de publicación, checksum o revisión.
+
+Se lee una versión del flujo y se libera el bloqueo antes de evaluar. Body UTF-8
+máximo 1 MiB, sin claves duplicadas; requestData/event.data y cada resultado tienen
+límite 256 KiB. Aplica SimulationInput/SimulationResult del contrato. Un error de
+sintaxis es 400, tamaño 413, contrato/tipos/ambigüedad 422; rol de negocio del fixture
+no permitido 403 y evento inexistente 409. Respuesta no-store.
+
+La simulación recorre una transición: valida datos de estado actual (dataSchema),
+evento, roles/guardia y mappings de entradas; luego valida fixtures de resultados
+contra contratos/perfiles del catálogo confiable. results usa exactamente los
+resultKey de acciones externas seleccionadas; no se permiten resultados sobrantes
+ni omitidos. NO_OP toma output de su definición y no necesita una entrada results.
+Sigue prohibido publicar/ejecutar NO_OP en el worker.
+
+Los drafts usan versión de contrato/perfil indicada o default del catálogo. Las
+publicadas usan sus versiones fijadas. No requiere rutas HTTP ni secretos; catálogo
+de contratos sí. No resuelve referencias de schemas a red/archivos. Las guardias se
+evalúan antes de resultados, como al aceptar un evento real. Las decisiones, updates,
+validación final y reglas coincidentes usan TransitionEvaluator compartido con el
+worker. UNIQUE ambiguo devuelve AMBIGUOUS_DECISION, no una ruta de negocio.
+
+No crea solicitudes, ejecuciones, checkpoints, referencias documentales, auditorías
+ni eventos. No invoca proveedores ni ejecuta reintentos. Las respuestas documentales
+son fixtures, no prueba de propiedad/antivirus de un documento real. La simulación
+valida MIME/tamaño/tipo configurados, no verifica almacenamiento o consentimiento.
+No equivale a publicar un draft válido ni a un recorrido persistente de demo.
+
+Ejemplo: [simulate.http](../examples/http/simulate.http). FlowSimulationTest cubre
+fronteras, contrato, guardias, NO_OP y ambigüedad; SimulationIT comprueba JWT/ACL,
+schemas HTTP, límites y que los conteos/revisión permanecen iguales tras simular.
diff --git a/specs/WORKER.md b/specs/WORKER.md
new file mode 100644
index 0000000..c94e7c9
--- /dev/null
+++ b/specs/WORKER.md
@@ -0,0 +1,121 @@
+# Worker durable y consulta de ejecución — T10
+
+El worker usa PostgreSQL como cola durable. `WORKER_ENABLED=true` activa polling,
+reclamación, renovación y recuperación; false por defecto. Es independiente de
+REQUEST_API_ENABLED y FLOW_ADMIN_ENABLED, pero siempre exige el runtime DB
+restringido y Flyway separado. Puede desplegarse como proceso solo worker usando
+la misma imagen. No se ha desplegado esta versión.
+
+Defaults: 10 workers, lote máximo 10, polling 500 ms, lease 60 s y renovación cada
+15 s. Variables WORKER_CONCURRENCY/BATCH/POLL_MS/LEASE_SECONDS/RENEW_SECONDS en
+.env.example y compose.yaml. La renovación debe ser menor que la mitad del lease.
+El scheduler reclama únicamente capacidad libre; no acumula una cola local de
+trabajos ya reclamados que caduquen esperando un thread. Los executors locales
+despachan trabajo; execution/action_execution conservan toda la evidencia durable.
+
+## Protocolo y concurrencia
+
+Un CTE con FOR UPDATE SKIP LOCKED reclama PENDING/RETRY_WAIT vencidos y RUNNING con
+lease expirado. Orden next_attempt_at/created_at/id, límite de lote. En la misma
+transacción incrementa attempts/fencing_token y fija owner/lease con clock_timestamp
+de PostgreSQL. Se confirma antes de evaluar o invocar un adapter.
+
+Renovar requiere RUNNING, executionId/requestId, owner y token exactos, y lease aún
+vigente. Un lease vencido no revive por renovación. Reclamarlo aumenta el token;
+el propietario anterior no puede cargar, iniciar/guardar checkpoints, fallar ni
+confirmar el trabajo. Escrituras de checkpoint bloquean la ejecución y verifican
+lease con tiempo DB al inicio y antes del commit. Finalización/fallo bloquean
+solicitud y después ejecución; no hay transacciones abiertas mientras corre el adapter.
+
+El worker carga estado/revisión aceptados y la definición por flow_id fijado.
+Rechaza una solicitud modificada antes de llamar al adapter y vuelve a comprobar
+estado, revisión, versión y lease al finalizar. accepted_revision es la revisión
+posterior a aceptar T09. Un worker obsoleto que retorna después de otro no puede
+sobrescribir el resultado, aunque el proveedor haya procesado su llamada.
+
+## Acciones y checkpoints
+
+WorkerActions es un registro de adapters confiable, consciente de versión, que
+declara capability/schemas/effect y ejecuta la acción. ActionContext contiene inputs
+resueltos, identidad mínima y clave externa estable `executionId:actionId`.
+No se aceptan capacidades ni URLs del evento. NO_OP sigue prohibido fuera de simulación.
+
+Antes de invocar se valida input y persiste checkpoint RUNNING, input_hash SHA-256,
+clave externa, clasificación de efecto e intento. Al retornar, valida output y lo
+guarda SUCCEEDED antes de avanzar. Tras recuperación, SUCCEEDED se reutiliza solo
+si hash/clave/effect coinciden; no vuelve a ejecutar el adapter. Los mappings de
+acciones posteriores usan los resultados recuperados. Cambio incompatible bloquea
+con error de contrato y conciliación; nunca se usa silenciosamente un resultado ajeno.
+
+V3 agrega action_execution.effect. Lo declara el adapter confiable, no el flujo.
+Checkpoints anteriores a V3 se clasifican conservadoramente NON_IDEMPOTENT_WRITE;
+no se inventa que eran seguros. V1/V2 no se alteran. Migración/rollback y permisos
+se verifican contra PostgreSQL 9.6.2 en la base de pruebas.
+
+Recuperación inicial:
+
+- Acción READ_ONLY o IDEMPOTENT_WRITE en curso puede retomarse con misma clave y
+ hash, respetando maxAttempts del contrato de acción.
+- NON_IDEMPOTENT_WRITE RUNNING o UNKNOWN no se repite: ejecución pasa a
+ RECONCILIATION_REQUIRED y la solicitud permanece bloqueada.
+- Un fallo posterior a escrituras exitosas o pendientes tampoco libera la
+ solicitud como FAILED; se conserva para conciliación.
+- Fallo de persistencia final deja RUNNING/checkpoints existentes para recuperación
+ por lease. Al reanudar no se repiten acciones ya SUCCEEDED.
+
+T12 programa reintentos transitorios seguros con backoff/jitter y Retry-After
+acotados. El checkpoint del intento, RETRY_WAIT y auditoría se confirman juntos;
+no se pierden éxitos anteriores. Agotamiento seguro termina FAILED; escrituras
+inciertas conservan el bloqueo para resolución administrativa. RECONCILIATION.md
+documenta límites, evidencia, replay y autorización.
+Los adapters T11 limitan sus timeouts y presupuesto de ejecución por debajo
+del lease y respetar la idempotencia externa; el token local no evita duplicados
+en el proveedor.
+
+## Finalización
+
+Tras las acciones, el evaluador puro T05 decide destino/outcome. Los updates se
+calculan desde el snapshot original y resultados de acciones, se aplican sobre una
+copia y se valida dataSchema y límite de 256 KiB antes de confirmar. JSON Pointer
+admite objetos y posiciones existentes de arrays; no crea índices dispersos ni
+interpreta append `-`. Padres de objeto ausentes se crean como objetos y el schema
+final decide su validez. Nunca se actualiza identidad/ACL desde estos mappings.
+
+Transacción final comprueba todos los checkpoints esperados SUCCEEDED y actualiza
+data/estado/revisión de solicitud, resultado SUCCEEDED de execution y auditoría
+TRANSITION_COMPLETED con transitionId, actionIds, hitPolicy y ruleIds. Cualquier
+fallo revierte esos cambios juntos, conservando los checkpoints previos.
+
+## Consulta y operación
+
+GET /api/v1/executions/{id} ya implementado bajo REQUEST_API_ENABLED. Reutiliza
+ResourceAccess y filtra owner/scope en SQL. Retorna ExecutionView con estado,
+resultado de negocio permitido y código de error seguro; no devuelve inputs de
+acciones, actor_snapshot, tokens, lease_owner ni respuesta cruda del proveedor.
+Recurso ajeno/inexistente retorna 404. Location de T09 ya puede consultarse.
+
+Apagado: detiene reclamación, conserva renovaciones durante hasta 20 s de drenaje,
+interrumpe trabajos restantes y deja de renovar. Checkpoints/leases permiten la
+recuperación. Un heartbeat que termina tarde no interrumpe otro trabajo que reutilice
+el thread. Los adapters deben tener tiempo de espera finito; la interrupción no
+garantiza cancelar un efecto externo, por eso se conserva la clasificación durable.
+
+T11 incorpora adapters HTTP y verificación documental en el registro productivo.
+Una acción sin contrato/ruta configurados falla con INTEGRATION_UNAVAILABLE.
+Los catálogos predeterminados están vacíos. Configurar contratos y rutas antes de
+habilitar integraciones; consultar INTEGRATIONS.md. NO_OP no puede ejecutarse.
+
+## Evidencia y límites
+
+WorkerIT usa PostgreSQL real/JWT local y adapters exclusivamente de prueba. Las
+barreras verifican reclamantes concurrentes, SKIP LOCKED, fencing, retorno tardío,
+renovación durante acción, checkpoints tras caída, clave externa estable, efecto
+no seguro, rollback final, decisiones y autorización HTTP. SimulatedCrash abandona
+el intento sin guardar resultado; se reconstruye el adapter de persistencia y se
+recupera desde DB. No se anuncia una prueba de caída del proceso/JVM ni integración
+real con proveedor. La idempotencia del proveedor se demuestra solo con un doble
+explícito que deduplica por clave; T11/T16 comprobarán los contratos externos.
+
+Ejecutar `mvnw.cmd clean verify -Pmigration-tests-external` con credenciales solo en
+MIGRATION_TEST_DB_* y base orquestador_test. Sin ese perfil, verify necesita Docker
+para Testcontainers. Los esquemas y roles temporales propios se eliminan al finalizar.
diff --git a/specs/acceptance-cases.json b/specs/acceptance-cases.json
new file mode 100644
index 0000000..a65da3b
--- /dev/null
+++ b/specs/acceptance-cases.json
@@ -0,0 +1,77 @@
+{
+ "A01": [
+ "DemoFlowsIT#creditBoundaries"
+ ],
+ "A02": [
+ "DemoFlowsIT#missingCustomerRemainsCreatedAndCanBeRetried"
+ ],
+ "A03": [
+ "DemoFlowsIT#customerApiFailureRetriesAndNeverBecomesCustomerNotFound"
+ ],
+ "A04": [
+ "DemoFlowsIT#creditBoundaries",
+ "DecisionEvaluatorTest#a04ScoringUsesActualContractExampleAndExactDecimalBoundaries"
+ ],
+ "A05": [
+ "DemoFlowsIT#invalidProviderScoreNeverDefaultsToManualReview"
+ ],
+ "A06": [
+ "DecisionEvaluatorTest#uniqueRejectsMultipleMatchesEvenWithIdenticalOutputAndFirstPreservesDeclaredOrder",
+ "FlowSimulationTest#ambiguousDraftDecisionIsNotConvertedIntoDefault"
+ ],
+ "A07": [
+ "DemoFlowsIT#repeatedEventHasOneExecutionAndOneProviderCallEvenAfterCompletion"
+ ],
+ "A08": [
+ "EventAcceptanceIT#concurrentDifferentPayloadsConflictAndNumericCanonicalReplayIsStable"
+ ],
+ "A09": [
+ "EventAcceptanceIT#concurrentIdenticalAndDifferentKeysHaveOneAcceptance"
+ ],
+ "A10": [
+ "DemoFlowsIT#existingCreditKeepsV1WhileNewCreditRequiresLicense"
+ ],
+ "A11": [
+ "DocumentTasksIT#invalidRetainsTaskAndReasonThenValidCompletesWithoutDuplicateReference",
+ "DocumentTasksIT#untrustedOrNoncompliantMetadataNeverCreatesReference",
+ "DocumentTasksIT#anExistingReferenceCannotBeReassignedEvenIfRemoteMetadataDisagrees"
+ ],
+ "A12": [
+ "RetryReconciliationIT#realChildJvmCrashAtEffectBoundaryRecoversOrRequiresResolution",
+ "WorkerIT#durableCheckpointSurvivesAdapterRestartAndUsesStableExternalKey"
+ ],
+ "A13": [
+ "RetryReconciliationIT#realChildJvmCrashAtEffectBoundaryRecoversOrRequiresResolution",
+ "WorkerIT#idempotentWriteReusesProviderKeyAfterLostCheckpoint"
+ ],
+ "A14": [
+ "WorkerIT#twoClaimantsHaveDisjointJobsAndLockedRowsAreSkipped",
+ "WorkerIT#renewalAndReclaimFenceEveryStaleMutation",
+ "WorkerIT#oldWorkerReturningAfterNewWorkerCannotOverwriteResult"
+ ],
+ "A15": [
+ "EventAcceptanceIT#ownerScopeAndTransitionRolesCannotBeForged",
+ "ObservabilityIT#historyAuthorizationAndCursorBindingNeverExposeForeignResource",
+ "SimulationIT#simulationIsAuthorizedReadOnlyAndConformsToContract"
+ ],
+ "A16": [
+ "FlowPublicationIT#databaseAndApplicationBothPreventPublishedMutation",
+ "SecurityHttpIT#rolesScopeAndImmutablePublicationOverHttp"
+ ],
+ "A17": [
+ "FlowPublicationIT#activationRetirementRaceNeverLeavesRetiredVersionActive",
+ "FlowPublicationIT#retirementBeforeAssignmentPreventsANewRequestEvenWhenTransactionsOverlap"
+ ],
+ "A18": [
+ "DemoFlowsIT#existingCreditKeepsV1WhileNewCreditRequiresLicense",
+ "DemoFlowTest#exactExampleIsPublishable"
+ ],
+ "A19": [
+ "WorkerIT#finalCommitFailureRollsBackAndRecoveryDoesNotRepeatSucceededAction",
+ "RetryReconciliationIT#retryAuditFailureRollsBackScheduleAndRecovers",
+ "RetryReconciliationIT#resolutionAuditFailureRollsBackCheckpointLedgerAndQueue"
+ ],
+ "A20": [
+ "DocumentTasksIT#clientCannotSubmitProviderFieldsOrReplaceMetadata"
+ ]
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/application/port/ActionHandler.java b/src/main/java/com/organizacion/orchestrator/action/application/port/ActionHandler.java
new file mode 100644
index 0000000..8eb7e49
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/application/port/ActionHandler.java
@@ -0,0 +1,8 @@
+package com.organizacion.orchestrator.action.application.port;
+
+import com.organizacion.orchestrator.action.domain.*;
+
+public interface ActionHandler {
+ String type();
+ ActionResult execute(ActionDefinition definition, ActionContext context);
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/domain/ActionCapability.java b/src/main/java/com/organizacion/orchestrator/action/domain/ActionCapability.java
new file mode 100644
index 0000000..8b073d9
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/domain/ActionCapability.java
@@ -0,0 +1,15 @@
+package com.organizacion.orchestrator.action.domain;
+
+import com.organizacion.orchestrator.flow.domain.InlineSchema;
+import com.organizacion.orchestrator.shared.domain.Checks;
+import java.util.Objects;
+
+/** Trusted deployment catalog entry. Never deserialized from an editable flow. */
+public record ActionCapability(String type, Effect effect, InlineSchema configurationSchema,
+ InlineSchema inputSchema, InlineSchema outputSchema) {
+ public enum Effect { READ_ONLY, IDEMPOTENT_WRITE, NON_IDEMPOTENT_WRITE }
+ public ActionCapability {
+ Checks.key(type); Objects.requireNonNull(effect); Objects.requireNonNull(configurationSchema);
+ Objects.requireNonNull(inputSchema); Objects.requireNonNull(outputSchema);
+ }
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/domain/ActionContext.java b/src/main/java/com/organizacion/orchestrator/action/domain/ActionContext.java
new file mode 100644
index 0000000..60fb74c
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/domain/ActionContext.java
@@ -0,0 +1,16 @@
+package com.organizacion.orchestrator.action.domain;
+
+import com.organizacion.orchestrator.shared.domain.*;
+import com.organizacion.orchestrator.shared.domain.Identifiers.*;
+import com.organizacion.orchestrator.shared.domain.JsonValue.ObjectValue;
+import java.util.Objects;
+
+/** Inputs already resolved and validated by the application; stable external key survives retries. */
+public record ActionContext(ExecutionId executionId, RequestId requestId, Actor actor,
+ ObjectValue inputs, String externalIdempotencyKey, int attempt) {
+ public ActionContext {
+ Objects.requireNonNull(executionId); Objects.requireNonNull(requestId);
+ Objects.requireNonNull(actor); Objects.requireNonNull(inputs);
+ Checks.text(externalIdempotencyKey, 200); Checks.positive(attempt);
+ }
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/domain/ActionDefinition.java b/src/main/java/com/organizacion/orchestrator/action/domain/ActionDefinition.java
new file mode 100644
index 0000000..c90111e
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/domain/ActionDefinition.java
@@ -0,0 +1,43 @@
+package com.organizacion.orchestrator.action.domain;
+
+import com.organizacion.orchestrator.decision.domain.ValueExpression;
+import com.organizacion.orchestrator.shared.domain.Checks;
+import com.organizacion.orchestrator.shared.domain.JsonValue.ObjectValue;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/** Editable configuration deliberately excludes URLs, credentials and effect classification. */
+public sealed interface ActionDefinition {
+ String id();
+ String resultKey();
+ String type();
+
+ record Integration(String id, String resultKey, String integrationKey,
+ Optional contractVersion, Optional profile, Optional profileVersion,
+ Map inputs, RetryPolicy retry) implements ActionDefinition {
+ public Integration {
+ Checks.key(id); Checks.key(resultKey); Checks.key(integrationKey);
+ Objects.requireNonNull(contractVersion).ifPresent(v -> Checks.text(v, 100));
+ Objects.requireNonNull(profile).ifPresent(Checks::key);
+ Objects.requireNonNull(profileVersion).ifPresent(v -> Checks.text(v, 100));
+ if (profileVersion.isPresent() && profile.isEmpty()) throw new IllegalArgumentException("Profile version without profile");
+ inputs = Map.copyOf(inputs); inputs.keySet().forEach(Checks::key);
+ Objects.requireNonNull(retry);
+ }
+ public String type() { return "INVOKE_INTEGRATION"; }
+ }
+ record RegisterDocument(String id, String resultKey, String documentType,
+ ValueExpression documentId, RetryPolicy retry) implements ActionDefinition {
+ public RegisterDocument {
+ Checks.key(id); Checks.key(resultKey); Checks.key(documentType);
+ Objects.requireNonNull(documentId); Objects.requireNonNull(retry);
+ }
+ public String type() { return "REGISTER_DOCUMENT"; }
+ }
+ /** Only simulation may execute this action. Publication must reject it. */
+ record NoOp(String id, String resultKey, ObjectValue output) implements ActionDefinition {
+ public NoOp { Checks.key(id); Checks.key(resultKey); Objects.requireNonNull(output); }
+ public String type() { return "NO_OP"; }
+ }
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/domain/ActionResult.java b/src/main/java/com/organizacion/orchestrator/action/domain/ActionResult.java
new file mode 100644
index 0000000..985dc2e
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/domain/ActionResult.java
@@ -0,0 +1,25 @@
+package com.organizacion.orchestrator.action.domain;
+
+import com.organizacion.orchestrator.shared.domain.*;
+import com.organizacion.orchestrator.shared.domain.JsonValue.ObjectValue;
+import java.util.Objects;
+import java.util.Optional;
+
+public sealed interface ActionResult {
+ /** Includes normalized negative business outcomes such as found=false or status=INVALID. */
+ record Completed(ObjectValue output, Optional providerReference) implements ActionResult {
+ public Completed {
+ Objects.requireNonNull(output);
+ Objects.requireNonNull(providerReference).ifPresent(v -> Checks.text(v, 200));
+ }
+ }
+ enum FailureKind { TRANSIENT, PERMANENT, UNKNOWN_EFFECT }
+ /** Retry eligibility also depends on trusted capability and checkpoints, not just kind. */
+ record TechnicalFailure(ErrorCode code, FailureKind kind, Optional retryAfter) implements ActionResult {
+ public TechnicalFailure(ErrorCode code,FailureKind kind){this(code,kind,Optional.empty());}
+ public TechnicalFailure {
+ Objects.requireNonNull(code); Objects.requireNonNull(kind); Objects.requireNonNull(retryAfter);
+ retryAfter.ifPresent(d->{if(d.isNegative()||d.compareTo(java.time.Duration.ofSeconds(120))>0)throw new IllegalArgumentException("Invalid retry hint");});
+ }
+ }
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/domain/RetryDelay.java b/src/main/java/com/organizacion/orchestrator/action/domain/RetryDelay.java
new file mode 100644
index 0000000..4853257
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/domain/RetryDelay.java
@@ -0,0 +1,15 @@
+package com.organizacion.orchestrator.action.domain;
+
+import java.time.Duration;
+import java.util.Optional;
+
+/** Bounded exponential delay. Caller supplies randomness, making boundary tests deterministic. */
+public final class RetryDelay {
+ private RetryDelay(){}
+ public static Duration after(RetryPolicy policy,int completedAttempts,Optional hint,double random){
+ if(completedAttempts<1||completedAttempts>=policy.maxAttempts()||random<0||random>=1)throw new IllegalArgumentException("No eligible retry");
+ long base=Math.min(policy.maxBackoff().toMillis(),policy.initialBackoff().toMillis()*(1L<<(completedAttempts-1)));
+ long jittered=Math.min(policy.maxBackoff().toMillis(),Math.max(100,(long)(base*(0.8+random*0.4))));
+ return Duration.ofMillis(Math.min(120000,Math.max(jittered,hint.map(Duration::toMillis).orElse(0L))));
+ }
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/domain/RetryPolicy.java b/src/main/java/com/organizacion/orchestrator/action/domain/RetryPolicy.java
new file mode 100644
index 0000000..50a63dd
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/domain/RetryPolicy.java
@@ -0,0 +1,17 @@
+package com.organizacion.orchestrator.action.domain;
+
+import java.time.Duration;
+import java.util.Objects;
+
+public record RetryPolicy(int maxAttempts, Duration initialBackoff, Duration maxBackoff) {
+ public static final RetryPolicy DEFAULT = new RetryPolicy(3, Duration.ofSeconds(2), Duration.ofSeconds(4));
+ public RetryPolicy {
+ Objects.requireNonNull(initialBackoff); Objects.requireNonNull(maxBackoff);
+ if (maxAttempts < 1 || maxAttempts > 3
+ || initialBackoff.compareTo(Duration.ofMillis(100)) < 0
+ || initialBackoff.compareTo(Duration.ofSeconds(60)) > 0
+ || maxBackoff.compareTo(initialBackoff) < 0
+ || maxBackoff.compareTo(Duration.ofSeconds(120)) > 0)
+ throw new IllegalArgumentException("Invalid bounded retry policy");
+ }
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/infrastructure/NoOpSimulationHandler.java b/src/main/java/com/organizacion/orchestrator/action/infrastructure/NoOpSimulationHandler.java
new file mode 100644
index 0000000..6cf945b
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/infrastructure/NoOpSimulationHandler.java
@@ -0,0 +1,14 @@
+package com.organizacion.orchestrator.action.infrastructure;
+
+import com.organizacion.orchestrator.action.application.port.ActionHandler;
+import com.organizacion.orchestrator.action.domain.*;
+import java.util.Optional;
+
+/** Explicit simulation-only utility. Never a Spring bean or member of the worker registry. */
+public final class NoOpSimulationHandler implements ActionHandler {
+ public String type(){return "NO_OP";}
+ public ActionResult execute(ActionDefinition definition,ActionContext context){
+ if(!(definition instanceof ActionDefinition.NoOp noOp))throw new IllegalArgumentException("Simulation NO_OP required");
+ return new ActionResult.Completed(noOp.output(),Optional.empty());
+ }
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/infrastructure/RegisterDocumentHandler.java b/src/main/java/com/organizacion/orchestrator/action/infrastructure/RegisterDocumentHandler.java
new file mode 100644
index 0000000..d759460
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/infrastructure/RegisterDocumentHandler.java
@@ -0,0 +1,36 @@
+package com.organizacion.orchestrator.action.infrastructure;
+
+import com.organizacion.orchestrator.action.application.port.ActionHandler;
+import com.organizacion.orchestrator.action.domain.*;
+import com.organizacion.orchestrator.flow.domain.PublicationCatalog;
+import com.organizacion.orchestrator.integration.infrastructure.HttpIntegrationHandler;
+import com.organizacion.orchestrator.shared.domain.*;
+import com.organizacion.orchestrator.shared.domain.JsonValue.*;
+import java.util.*;
+
+/** Verifies metadata through fixed DOCUMENTS_METADATA/v1 protocol. The worker persists the reference under fencing. */
+public final class RegisterDocumentHandler implements ActionHandler {
+ private final PublicationCatalog catalog;private final HttpIntegrationHandler metadata;
+ public RegisterDocumentHandler(PublicationCatalog catalog,HttpIntegrationHandler metadata){this.catalog=catalog;this.metadata=metadata;}
+ public String type(){return "REGISTER_DOCUMENT";}
+ private ActionDefinition.Integration lookup(ActionDefinition.RegisterDocument d){return new ActionDefinition.Integration(d.id(),d.resultKey(),"DOCUMENTS_METADATA",Optional.of("1"),Optional.empty(),Optional.empty(),Map.of(),d.retry());}
+ public Optional capability(ActionDefinition.RegisterDocument definition){
+ var capability=catalog.handlers().get(type());var lookup=metadata.capability(lookup(definition));
+ if(capability==null||capability.effect()!=ActionCapability.Effect.READ_ONLY||!catalog.documents().containsKey(definition.documentType())
+ ||lookup.isEmpty()||lookup.get().effect()!=ActionCapability.Effect.READ_ONLY)return Optional.empty();return Optional.of(capability);
+ }
+ public ActionResult execute(ActionDefinition definition,ActionContext context){
+ if(!(definition instanceof ActionDefinition.RegisterDocument document)||capability(document).isEmpty())return fail(ErrorCode.INTEGRATION_UNAVAILABLE);
+ try{
+ String id=((StringValue)context.inputs().fields().get("documentId")).value();UUID parsed=UUID.fromString(id);
+ var input=new ObjectValue(Map.of("documentId",new StringValue(parsed.toString()),"requestId",new StringValue(context.requestId().value().toString()),"businessScope",new StringValue(context.actor().businessScope())));
+ var response=metadata.execute(lookup(document),new ActionContext(context.executionId(),context.requestId(),context.actor(),input,context.externalIdempotencyKey(),context.attempt()));
+ if(!(response instanceof ActionResult.Completed completed))return response;
+ var policy=catalog.documents().get(document.documentType());
+ var verified=com.organizacion.orchestrator.document.domain.DocumentMetadata.verify(parsed,context.requestId().value(),context.actor().businessScope(),document.documentType(),policy.mimeTypes(),policy.maxBytes(),completed.output());
+ return new ActionResult.Completed(verified,Optional.empty());
+ }catch(DomainException e){return fail(e.code());
+ }catch(RuntimeException e){return fail(ErrorCode.INTEGRATION_CONTRACT_ERROR);}
+ }
+ private static ActionResult fail(ErrorCode code){return new ActionResult.TechnicalFailure(code,ActionResult.FailureKind.PERMANENT);}
+}
diff --git a/src/main/java/com/organizacion/orchestrator/action/infrastructure/RegisteredWorkerActions.java b/src/main/java/com/organizacion/orchestrator/action/infrastructure/RegisteredWorkerActions.java
new file mode 100644
index 0000000..132ec6a
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/action/infrastructure/RegisteredWorkerActions.java
@@ -0,0 +1,22 @@
+package com.organizacion.orchestrator.action.infrastructure;
+
+import com.organizacion.orchestrator.execution.application.port.WorkerActions;
+import com.organizacion.orchestrator.action.domain.*;
+import com.organizacion.orchestrator.integration.infrastructure.HttpIntegrationHandler;
+import com.organizacion.orchestrator.shared.domain.ErrorCode;
+import java.util.Optional;
+
+public final class RegisteredWorkerActions implements WorkerActions {
+ private final HttpIntegrationHandler integrations;private final RegisterDocumentHandler documents;
+ public RegisteredWorkerActions(HttpIntegrationHandler integrations,RegisterDocumentHandler documents){this.integrations=integrations;this.documents=documents;}
+ public Optional capability(ActionDefinition definition){return switch(definition){
+ case ActionDefinition.Integration i->integrations.capability(i);
+ case ActionDefinition.RegisterDocument d->documents.capability(d);
+ case ActionDefinition.NoOp ignored->Optional.empty();
+ };}
+ public ActionResult execute(ActionDefinition definition,ActionContext context){return switch(definition){
+ case ActionDefinition.Integration i->integrations.execute(i,context);
+ case ActionDefinition.RegisterDocument d->documents.execute(d,context);
+ case ActionDefinition.NoOp ignored->new ActionResult.TechnicalFailure(ErrorCode.ACTION_FAILED,ActionResult.FailureKind.PERMANENT);
+ };}
+}
diff --git a/src/main/java/com/organizacion/orchestrator/audit/api/HistoryController.java b/src/main/java/com/organizacion/orchestrator/audit/api/HistoryController.java
new file mode 100644
index 0000000..b3a36c4
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/audit/api/HistoryController.java
@@ -0,0 +1,37 @@
+package com.organizacion.orchestrator.audit.api;
+
+import com.organizacion.orchestrator.audit.infrastructure.HistoryStore;
+import com.organizacion.orchestrator.flow.infrastructure.FlowJsonCodec;
+import com.organizacion.orchestrator.shared.application.ResourceAccess;
+import com.organizacion.orchestrator.shared.application.port.ResourceOwnershipStore.Kind;
+import com.organizacion.orchestrator.shared.infrastructure.security.*;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.*;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.web.bind.annotation.*;
+import javax.sql.DataSource;
+import java.time.Instant;
+import java.util.UUID;
+
+@RestController
+@ConditionalOnProperty(name="orchestrator.requests.enabled",havingValue="true")
+public class HistoryController {
+ private final HistoryStore store;private final ResourceAccess access;private final JwtActorMapper actors;
+ public HistoryController(DataSource source,FlowJsonCodec codec,ResourceAccess access,JwtActorMapper actors,RuntimeRoleVerifier verified,
+ @Value("${orchestrator.database.schema:orchestrator}")String schema){this.store=new HistoryStore(source,schema,codec);this.access=access;this.actors=actors;}
+ @GetMapping(value="/api/v1/requests/{id}/history",produces=MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity get(@PathVariable UUID id,@AuthenticationPrincipal Jwt jwt,
+ @RequestParam(defaultValue="50")int limit,@RequestParam(required=false)String cursor,
+ @RequestParam(required=false)String kind,@RequestParam(required=false)UUID executionId,
+ @RequestParam(required=false)String from,@RequestParam(required=false)String to){
+ access.require(actors.actor(jwt),Kind.REQUEST,id);
+ return ResponseEntity.ok().cacheControl(CacheControl.noStore()).body(store.page(id,limit,kind,executionId,instant(from),instant(to),cursor));
+ }
+ private static Instant instant(String value){
+ if(value==null)return null;
+ try{if(!value.endsWith("Z"))throw new IllegalArgumentException("UTC required");return Instant.parse(value);}
+ catch(java.time.DateTimeException e){throw new IllegalArgumentException("Invalid time");}
+ }
+}
diff --git a/src/main/java/com/organizacion/orchestrator/audit/infrastructure/HistoryStore.java b/src/main/java/com/organizacion/orchestrator/audit/infrastructure/HistoryStore.java
new file mode 100644
index 0000000..4f0463f
--- /dev/null
+++ b/src/main/java/com/organizacion/orchestrator/audit/infrastructure/HistoryStore.java
@@ -0,0 +1,55 @@
+package com.organizacion.orchestrator.audit.infrastructure;
+
+import com.fasterxml.jackson.databind.node.*;
+import com.organizacion.orchestrator.flow.infrastructure.FlowJsonCodec;
+import org.springframework.jdbc.core.JdbcTemplate;
+import javax.sql.DataSource;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.*;
+import java.nio.charset.StandardCharsets;
+import java.security.*;
+
+/** Read-only keyset projection. Cursor is a position, never an authorization credential. */
+public final class HistoryStore {
+ private final JdbcTemplate jdbc;private final String s;private final FlowJsonCodec codec;
+ private static final Set DETAILS=Set.of("correlationId","transitionId","acceptedRevision","ruleIds","reasonCodes","actionIds","hitPolicy","code","actionId","delayMs","mode","resolutionId");
+ public HistoryStore(DataSource source,String schema,FlowJsonCodec codec){
+ if(!schema.matches("[a-z_][a-z0-9_]*"))throw new IllegalArgumentException("Invalid schema");
+ jdbc=new JdbcTemplate(source);jdbc.setQueryTimeout(5);s="\""+schema+"\".";this.codec=codec;
+ }
+ public String page(UUID request,int limit,String kind,UUID execution,Instant from,Instant to,String cursor){
+ if(limit<1||limit>100||kind!=null&&!kind.matches("[A-Za-z][A-Za-z0-9_.-]{0,99}")||from!=null&&to!=null&&!from.isBefore(to))throw new IllegalArgumentException("Invalid filters");
+ String binding=digest(request+"|"+(kind==null?"":kind)+"|"+execution+"|"+from+"|"+to);
+ var args=new ArrayList