diff --git a/.dockerignore b/.dockerignore index 1619d57..40db09a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,5 +5,12 @@ !pom.xml !src/ !src/** +!contracts/ +!contracts/** +!schemas/ +!schemas/** +!examples/ +!examples/contracts/ +!examples/contracts/** !Dockerfile !.dockerignore diff --git a/Dockerfile b/Dockerfile index 112fb91..b1fe697 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,9 @@ COPY .mvn/ .mvn/ COPY mvnw pom.xml ./ RUN sed -i 's/\r$//' mvnw COPY src/ src/ +COPY contracts/ contracts/ +COPY schemas/ schemas/ +COPY examples/contracts/ examples/contracts/ # package ejecuta pruebas unitarias; la suite de integración se ejecuta en CI con verify. RUN sh ./mvnw --batch-mode --no-transfer-progress package diff --git a/README.md b/README.md index 50abd8d..6121692 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Arquetipo de un microservicio Spring Boot para ejecutar flujos configurables y v ## Estado -Esta entrega crea la estructura Maven, aplicación, configuración JWT, salud, configuración de persistencia y documentación de ejecución y seguimiento. El motor, endpoints de negocio, migraciones y flujos todavía están pendientes. Consultar [PROGRESS](specs/PROGRESS.md) para evidencia y bloqueos; no interpretar la estructura como implementación completa. +Incluye la estructura Maven, configuración JWT/PostgreSQL, Docker y salud verificada (T01), además del [contrato OpenAPI](contracts/openapi.yaml), [JSON Schema del DSL](schemas/flow-definition.schema.json) y ejemplos con validación automática (T02). El motor, endpoints de negocio, migraciones y flujos completos todavía están pendientes. Consultar [PROGRESS](specs/PROGRESS.md) para evidencia y bloqueos. ## Requisitos y ejecución @@ -31,9 +31,17 @@ Salud: `http://localhost:8080/actuator/health/readiness`. El puerto publicado se - [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`. La estructura inicial aún no contiene pruebas de negocio; T01 debe completar su smoke test y T03 incorporar Testcontainers. CI ejecuta el mismo `clean verify`. +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: -Los contratos, schemas, migraciones y ejemplos de flujos tienen directorios reservados. El Dockerfile compila con JDK 21 y ejecuta con JRE 21 como usuario no root. Compose no administra PostgreSQL ni sus volúmenes. Mocks y roles de mínimo privilegio siguen pendientes de T11 y T03/T07. +```powershell +.\mvnw.cmd clean verify -Pdeployment-smoke '-Dsmoke.baseUrl=http://server02:8787' +``` + +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. + +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. ## Límites funcionales previstos diff --git a/contracts/README.md b/contracts/README.md index 35149d5..40380cc 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,5 +1,34 @@ -# Contratos pendientes de T02 +# Contrato REST del orquestador -Crear openapi.yaml (OpenAPI 3.1) con las rutas de la sección 10 de la especificación, -JWT, ETag, If-Match, Idempotency-Key, errores problem+json y cursores. -Validar automáticamente contrato y ejemplos antes de cerrar T02. +`openapi.yaml` define OpenAPI 3.1.0 para las 14 operaciones de la sección 10 de la especificación. Base `/api/v1`, JWT Bearer obligatorio y autorización por recurso. Es un contrato objetivo: T02 no implementa controladores ni conecta proveedores. + +Los DTOs se referencian desde `../schemas/api.schema.json`; las definiciones de flujo desde `../schemas/flow-definition.schema.json`. Conservar esta estructura al abrir el contrato en un visor compatible con OpenAPI 3.1. No instalar springdoc ni habilitar Swagger UI en runtime todavía. Para usar un servidor estático, publicar juntos contracts/ y schemas/; abrir sólo un archivo sin sus referencias no basta. + +## Convenciones + +- UUID como string; fechas UTC con sufijo Z; campos opcionales ausentes se omiten, sin null salvo dentro de datos configurables. +- `data` es extensible, pero se valida también con el schema del flujo/evento. El DTO genérico no permite saltarse esa segunda validación. +- Identidad, scope, permisos, estado, revision y resultados externos no se toman de campos enviados por el cliente. Los wrappers de comandos rechazan propiedades adicionales. +- `If-Match` es un único ETag fuerte entre comillas, como `"7"`; falta: 428; desajuste: 412. Se exige en eventos, edición de borrador, publicación, retiro y activación. El ETag de activación pertenece al tipo. +- Idempotency-Key es obligatorio en creación, eventos y conciliación. Creación: unicidad actor/scope; eventos y conciliación: por recurso. Comando canónico distinto con misma clave: 409. En eventos se comprueba replay antes de revisión; se conserva executionId y se devuelve estado actual de la ejecución. +- Aceptación de evento devuelve 202 y Location. No supone éxito de negocio. SUCCEEDED puede contener CLIENTE_NO_ENCONTRADO, manteniendo el estado. +- `available-events` es orientativo. Durante una ejecución activa: events vacío y blockedByExecutionId presente. +- Historial ordenado por (createdAt,id), cursor opaco, máximo 100. Filtros kind, executionId, from inclusivo y to exclusivo; nextCursor se omite al terminar. +- Errores `application/problem+json` con status, code y correlationId; detalles filtrados. 404 oculta recursos de otro scope; 403 representa permiso insuficiente cuando revelar el recurso está permitido. +- `x-required-roles` enumera roles técnicos alternativos (OR); no reemplaza autorización por recurso. ANALISTA y otros roles de negocio se definen en las transiciones y se revalidan al aceptar. + +## Administración + +Creación/edición reciben la definición completa. flowKey y version forman identidad inmutable, incluso al editar el borrador. Validación semántica retorna 200 con valid=false y errores JSON Pointer; publicación rechaza una definición inválida con 422. La publicación fija contractVersion/profileVersion desde catálogo confiable antes del checksum. + +La simulación recibe actor/requestData/results de prueba, evalúa guardia/decisión/updates y no realiza HTTP ni efectos persistentes. Los fixtures de simulación nunca son datos confiables para el endpoint de eventos. + +Tipos de solicitud se provisionarán en T03/T06 mediante migraciones/configuración administrativa; este MVP no agrega un endpoint de alta de tipos. Activar un tipo inexistente retorna 404. Retiro conserva las solicitudes existentes y bloquea nuevas asignaciones concurrentes. + +Conciliación sólo permite RESUME con resultados verificados por acción o CLOSE_WITHOUT_TRANSITION con ausencia/compensación de efectos verificada. La evidencia es una referencia y motivo, no documentos personales ni secretos. El operador autorizado verifica la evidencia; el cliente no puede escoger estado destino. La resolución es idempotente y auditada. + +## Validación automática + +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. diff --git a/contracts/openapi.yaml b/contracts/openapi.yaml new file mode 100644 index 0000000..16d90b5 --- /dev/null +++ b/contracts/openapi.yaml @@ -0,0 +1,1474 @@ +openapi: 3.1.0 +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 + recurso obligatorios. Campos opcionales ausentes se omiten; no se usa null salvo en datos configurables. +servers: +- url: /api/v1 +security: +- bearerAuth: [] +tags: +- name: Requests +- name: Administration +paths: + /requests: + post: + 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 + dataSchema del flujo. Máximo de datos 256 KiB. No se aceptan estado, propietario o resultados de proveedor + como autoridad del cliente.' + tags: + - Requests + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + responses: + '201': + description: Crear solicitud y fijar flujo activo + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/RequestView + example: + id: 00000000-0000-0000-0000-000000000123 + typeKey: CREDITO + flow: + id: 00000000-0000-0000-0000-000000000456 + flowKey: SCORING_DEMO + version: 1 + currentState: BURO_PENDIENTE + data: + clienteId: CLI-DEMO + consentimientoId: CONS-DEMO + revision: 7 + createdAt: '2026-10-26T17:20:00Z' + updatedAt: '2026-10-26T17:20:00Z' + headers: + ETag: + description: Revisión fuerte, entre comillas, por ejemplo "7". + schema: + type: string + pattern: ^"[0-9]+"$ + Location: + description: URI del recurso creado o ejecución aceptada. + schema: + type: string + format: uri-reference + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '409': + $ref: '#/components/responses/Conflict' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '503': + $ref: '#/components/responses/Unavailable' + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/CreateRequest + example: + typeKey: CREDITO + data: + clienteId: CLI-DEMO + consentimientoId: CONS-DEMO + /requests/{id}: + get: + operationId: getRequest + summary: Consultar solicitud autorizada + description: Consultar solicitud autorizada + tags: + - Requests + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: Consultar solicitud autorizada + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/RequestView + example: + id: 00000000-0000-0000-0000-000000000123 + typeKey: CREDITO + flow: + id: 00000000-0000-0000-0000-000000000456 + flowKey: SCORING_DEMO + version: 1 + currentState: BURO_PENDIENTE + data: + clienteId: CLI-DEMO + consentimientoId: CONS-DEMO + revision: 7 + createdAt: '2026-10-26T17:20:00Z' + updatedAt: '2026-10-26T17:20:00Z' + headers: + ETag: + description: Revisión fuerte, entre comillas, por ejemplo "7". + schema: + type: string + pattern: ^"[0-9]+"$ + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '503': + $ref: '#/components/responses/Unavailable' + /requests/{id}/available-events: + get: + 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.' + tags: + - Requests + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: Consultar tareas y eventos permitidos + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/AvailableEvents + example: + requestId: 00000000-0000-0000-0000-000000000123 + revision: 8 + events: [] + blocked: [] + blockedByExecutionId: 00000000-0000-0000-0000-000000000789 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '503': + $ref: '#/components/responses/Unavailable' + /requests/{id}/events: + post: + operationId: acceptEvent + summary: Aceptar un evento durable + 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.' + tags: + - Requests + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/IfMatch' + responses: + '202': + description: Aceptar un evento durable + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/AcceptedEvent + example: + executionId: 00000000-0000-0000-0000-000000000789 + requestId: 00000000-0000-0000-0000-000000000123 + status: PENDING + headers: + Location: + description: URI del recurso creado o ejecución aceptada. + schema: + type: string + format: uri-reference + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '428': + $ref: '#/components/responses/PreconditionRequired' + '503': + $ref: '#/components/responses/Unavailable' + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/EventCommand + example: + event: REGISTRAR_LICENCIA + data: + documentId: 00000000-0000-0000-0000-000000000789 + /executions/{id}: + get: + operationId: getExecution + summary: Consultar ejecución autorizada + description: Verificar acceso a la solicitud asociada. SUCCEEDED es éxito técnico y puede contener resultado + de negocio desfavorable; los fallos asíncronos se consultan aquí. El cliente hace polling con backoff y + después actualiza solicitud/ETag. + tags: + - Requests + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: Consultar ejecución autorizada + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/ExecutionView + example: + id: 00000000-0000-0000-0000-000000000789 + requestId: 00000000-0000-0000-0000-000000000123 + event: ASIGNAR_CLIENTE + status: SUCCEEDED + result: + outcome: CLIENTE_NO_ENCONTRADO + fromState: CREADA + toState: CREADA + reasonCodes: + - NO_MATCH + createdAt: '2026-10-26T17:20:00Z' + updatedAt: '2026-10-26T17:20:00Z' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '503': + $ref: '#/components/responses/Unavailable' + /requests/{id}/history: + get: + 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. + tags: + - Requests + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/Cursor' + - $ref: '#/components/parameters/Limit' + - name: kind + in: query + schema: + type: string + pattern: ^[A-Za-z][A-Za-z0-9_.-]{0,99}$ + - name: executionId + in: query + schema: + type: string + format: uuid + - name: from + in: query + schema: + type: string + format: date-time + pattern: Z$ + - name: to + in: query + schema: + type: string + format: date-time + pattern: Z$ + responses: + '200': + description: Consultar auditoría paginada y filtrada + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/HistoryPage + example: + items: + - id: 00000000-0000-0000-0000-000000000789 + requestId: 00000000-0000-0000-0000-000000000123 + kind: EVENT_ACCEPTED + actorSubject: analista-demo + flowId: 00000000-0000-0000-0000-000000000456 + fromState: CREADA + detail: + transitionId: asignar-cliente + createdAt: '2026-10-26T17:20:00Z' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '503': + $ref: '#/components/responses/Unavailable' + /admin/flows: + post: + operationId: createDraft + summary: Crear borrador + description: Clave/versión únicas. Cuerpo máximo 1 MiB. El borrador puede tener errores semánticos que se + reportarán al validar; siempre debe cumplir el schema estructural. + tags: + - Administration + parameters: [] + responses: + '201': + description: Crear borrador + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/FlowView + example: + id: 00000000-0000-0000-0000-000000000456 + status: DRAFT + revision: 0 + definition: + schemaVersion: 1 + flowKey: SCORING_DEMO + version: 1 + initialState: BURO_PENDIENTE + dataSchema: + type: object + properties: + clienteId: + type: string + consentimientoId: + type: string + validaciones: + type: object + required: + - clienteId + - consentimientoId + additionalProperties: false + states: + - key: BURO_PENDIENTE + terminal: false + - key: PASO_N + terminal: true + - key: PASO_M + terminal: true + - key: REVISION_MANUAL + terminal: true + events: + - key: VALIDAR_BURO + inputSchema: + type: object + additionalProperties: false + transitions: + - id: consultar-buro + from: BURO_PENDIENTE + event: VALIDAR_BURO + allowedRoles: + - ANALISTA + actions: + - id: buro + type: INVOKE_INTEGRATION + integrationKey: BURO_CONSULTA + contractVersion: '1' + inputs: + clienteId: + path: /request/data/clienteId + 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: 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: [] + createdAt: '2026-10-26T17:20:00Z' + createdBy: editor-demo + headers: + ETag: + description: Revisión fuerte, entre comillas, por ejemplo "7". + schema: + type: string + pattern: ^"[0-9]+"$ + Location: + description: URI del recurso creado o ejecución aceptada. + schema: + type: string + format: uri-reference + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '409': + $ref: '#/components/responses/Conflict' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '503': + $ref: '#/components/responses/Unavailable' + x-required-roles: + - FLOW_EDITOR + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/flow-definition.schema.json + example: + schemaVersion: 1 + flowKey: SCORING_DEMO + version: 1 + initialState: BURO_PENDIENTE + dataSchema: + type: object + properties: + clienteId: + type: string + consentimientoId: + type: string + validaciones: + type: object + required: + - clienteId + - consentimientoId + additionalProperties: false + states: + - key: BURO_PENDIENTE + terminal: false + - key: PASO_N + terminal: true + - key: PASO_M + terminal: true + - key: REVISION_MANUAL + terminal: true + events: + - key: VALIDAR_BURO + inputSchema: + type: object + additionalProperties: false + transitions: + - id: consultar-buro + from: BURO_PENDIENTE + event: VALIDAR_BURO + allowedRoles: + - ANALISTA + actions: + - id: buro + type: INVOKE_INTEGRATION + integrationKey: BURO_CONSULTA + contractVersion: '1' + inputs: + clienteId: + path: /request/data/clienteId + 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: 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: [] + /admin/flows/{id}: + put: + operationId: updateDraft + summary: Reemplazar definición de borrador + description: Sólo DRAFT. flowKey/version no cambian; crear otra versión para cambiar identidad. Publicados/retirados + son inmutables. Máximo 1 MiB. + tags: + - Administration + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IfMatch' + responses: + '200': + description: Reemplazar definición de borrador + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/FlowView + example: + id: 00000000-0000-0000-0000-000000000456 + status: DRAFT + revision: 0 + definition: + schemaVersion: 1 + flowKey: SCORING_DEMO + version: 1 + initialState: BURO_PENDIENTE + dataSchema: + type: object + properties: + clienteId: + type: string + consentimientoId: + type: string + validaciones: + type: object + required: + - clienteId + - consentimientoId + additionalProperties: false + states: + - key: BURO_PENDIENTE + terminal: false + - key: PASO_N + terminal: true + - key: PASO_M + terminal: true + - key: REVISION_MANUAL + terminal: true + events: + - key: VALIDAR_BURO + inputSchema: + type: object + additionalProperties: false + transitions: + - id: consultar-buro + from: BURO_PENDIENTE + event: VALIDAR_BURO + allowedRoles: + - ANALISTA + actions: + - id: buro + type: INVOKE_INTEGRATION + integrationKey: BURO_CONSULTA + contractVersion: '1' + inputs: + clienteId: + path: /request/data/clienteId + 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: 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: [] + createdAt: '2026-10-26T17:20:00Z' + createdBy: editor-demo + headers: + ETag: + description: Revisión fuerte, entre comillas, por ejemplo "7". + schema: + type: string + pattern: ^"[0-9]+"$ + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '428': + $ref: '#/components/responses/PreconditionRequired' + '503': + $ref: '#/components/responses/Unavailable' + x-required-roles: + - FLOW_EDITOR + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/flow-definition.schema.json + example: + schemaVersion: 1 + flowKey: SCORING_DEMO + version: 1 + initialState: BURO_PENDIENTE + dataSchema: + type: object + properties: + clienteId: + type: string + consentimientoId: + type: string + validaciones: + type: object + required: + - clienteId + - consentimientoId + additionalProperties: false + states: + - key: BURO_PENDIENTE + terminal: false + - key: PASO_N + terminal: true + - key: PASO_M + terminal: true + - key: REVISION_MANUAL + terminal: true + events: + - key: VALIDAR_BURO + inputSchema: + type: object + additionalProperties: false + transitions: + - id: consultar-buro + from: BURO_PENDIENTE + event: VALIDAR_BURO + allowedRoles: + - ANALISTA + actions: + - id: buro + type: INVOKE_INTEGRATION + integrationKey: BURO_CONSULTA + contractVersion: '1' + inputs: + clienteId: + path: /request/data/clienteId + 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: 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: [] + /admin/flows/{id}/validate: + post: + operationId: validateFlow + summary: Validar configuración sin publicarla + description: Errores semánticos devuelven 200 con valid=false y JSON Pointers. Sin invocar proveedores. Incluye + grafo, catálogos, límites de AST y mappings. + tags: + - Administration + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: Validar configuración sin publicarla + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/ValidationReport + example: + valid: false + errors: + - pointer: /transitions/0/to + code: UNKNOWN_STATE + message: Estado destino inexistente. + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '503': + $ref: '#/components/responses/Unavailable' + x-required-roles: + - FLOW_EDITOR + - FLOW_PUBLISHER + /admin/flows/{id}/simulate: + post: + operationId: simulateFlow + 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. + tags: + - Administration + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: Simular evaluación pura + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/SimulationResult + example: + transitionId: consultar-buro + outcome: N + toState: PASO_N + data: + clienteId: CLI-DEMO + consentimientoId: CONS-DEMO + validaciones: + buro: + score: 71 + consultationId: SIM-001 + matchedRuleIds: + - alto + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '503': + $ref: '#/components/responses/Unavailable' + x-required-roles: + - FLOW_EDITOR + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/SimulationInput + example: + state: BURO_PENDIENTE + requestData: + clienteId: CLI-DEMO + consentimientoId: CONS-DEMO + event: + event: VALIDAR_BURO + data: {} + actor: + subject: analista-demo + roles: + - ANALISTA + businessScope: DEMO + results: + buro: + score: 71 + consultationId: SIM-001 + /admin/flows/{id}/publish: + post: + operationId: publishFlow + summary: Publicar versión inmutable + description: Valida semántica, resuelve versiones de contratos/perfiles, calcula SHA-256 del JSON canónico + y registra auditoría. Rechaza NO_OP. Debe ser DRAFT. + tags: + - Administration + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IfMatch' + responses: + '200': + description: Publicar versión inmutable + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/FlowView + example: + id: 00000000-0000-0000-0000-000000000456 + status: PUBLISHED + revision: 1 + definition: + schemaVersion: 1 + flowKey: SCORING_DEMO + version: 1 + initialState: BURO_PENDIENTE + dataSchema: + type: object + properties: + clienteId: + type: string + consentimientoId: + type: string + validaciones: + type: object + required: + - clienteId + - consentimientoId + additionalProperties: false + states: + - key: BURO_PENDIENTE + terminal: false + - key: PASO_N + terminal: true + - key: PASO_M + terminal: true + - key: REVISION_MANUAL + terminal: true + events: + - key: VALIDAR_BURO + inputSchema: + type: object + additionalProperties: false + transitions: + - id: consultar-buro + from: BURO_PENDIENTE + event: VALIDAR_BURO + allowedRoles: + - ANALISTA + actions: + - id: buro + type: INVOKE_INTEGRATION + integrationKey: BURO_CONSULTA + contractVersion: '1' + inputs: + clienteId: + path: /request/data/clienteId + 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: 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: [] + createdAt: '2026-10-26T17:20:00Z' + createdBy: editor-demo + checksum: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + publishedAt: '2026-10-26T17:20:00Z' + headers: + ETag: + description: Revisión fuerte, entre comillas, por ejemplo "7". + schema: + type: string + pattern: ^"[0-9]+"$ + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '428': + $ref: '#/components/responses/PreconditionRequired' + '503': + $ref: '#/components/responses/Unavailable' + x-required-roles: + - FLOW_PUBLISHER + /admin/request-types/{key}/active-flow: + put: + operationId: activateFlow + summary: Activar flujo de un tipo existente + description: If-Match corresponde al tipo, no al flujo. Sólo versión PUBLISHED. Tipos se provisionan mediante + migración/configuración administrativa; tipo inexistente retorna 404. Activación y retiro se serializan. + Nuevas solicitudes fijan esta versión; existentes no migran. + tags: + - Administration + parameters: + - $ref: '#/components/parameters/TypeKey' + - $ref: '#/components/parameters/IfMatch' + responses: + '200': + description: Activar flujo de un tipo existente + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/RequestTypeView + example: + typeKey: CREDITO + activeFlowId: 00000000-0000-0000-0000-000000000456 + enabled: true + revision: 1 + headers: + ETag: + description: Revisión fuerte, entre comillas, por ejemplo "7". + schema: + type: string + pattern: ^"[0-9]+"$ + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '428': + $ref: '#/components/responses/PreconditionRequired' + '503': + $ref: '#/components/responses/Unavailable' + x-required-roles: + - FLOW_ADMIN + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/ActivateFlow + example: + flowId: 00000000-0000-0000-0000-000000000456 + /admin/flows/{id}/retire: + post: + operationId: retireFlow + summary: Retirar versión para nuevas asignaciones + description: Sólo PUBLISHED a RETIRED. Bloquear asignaciones concurrentes; conservar ejecución de instancias + existentes. No borrar la definición. + tags: + - Administration + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IfMatch' + responses: + '200': + description: Retirar versión para nuevas asignaciones + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/FlowView + example: + id: 00000000-0000-0000-0000-000000000456 + status: RETIRED + revision: 1 + definition: + schemaVersion: 1 + flowKey: SCORING_DEMO + version: 1 + initialState: BURO_PENDIENTE + dataSchema: + type: object + properties: + clienteId: + type: string + consentimientoId: + type: string + validaciones: + type: object + required: + - clienteId + - consentimientoId + additionalProperties: false + states: + - key: BURO_PENDIENTE + terminal: false + - key: PASO_N + terminal: true + - key: PASO_M + terminal: true + - key: REVISION_MANUAL + terminal: true + events: + - key: VALIDAR_BURO + inputSchema: + type: object + additionalProperties: false + transitions: + - id: consultar-buro + from: BURO_PENDIENTE + event: VALIDAR_BURO + allowedRoles: + - ANALISTA + actions: + - id: buro + type: INVOKE_INTEGRATION + integrationKey: BURO_CONSULTA + contractVersion: '1' + inputs: + clienteId: + path: /request/data/clienteId + 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: 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: [] + createdAt: '2026-10-26T17:20:00Z' + createdBy: editor-demo + checksum: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + publishedAt: '2026-10-26T17:20:00Z' + headers: + ETag: + description: Revisión fuerte, entre comillas, por ejemplo "7". + schema: + type: string + pattern: ^"[0-9]+"$ + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '412': + $ref: '#/components/responses/PreconditionFailed' + '428': + $ref: '#/components/responses/PreconditionRequired' + '503': + $ref: '#/components/responses/Unavailable' + x-required-roles: + - FLOW_ADMIN + /admin/executions/{id}/resolve: + post: + operationId: resolveExecution + 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. + Auditoría obligatoria. + tags: + - Administration + parameters: + - $ref: '#/components/parameters/Id' + - $ref: '#/components/parameters/IdempotencyKey' + responses: + '200': + description: Conciliar una ejecución incierta + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/ReconciliationView + example: + resolutionId: 00000000-0000-0000-0000-000000000456 + executionId: 00000000-0000-0000-0000-000000000789 + status: PENDING + resolvedAt: '2026-10-26T17:20:00Z' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '503': + $ref: '#/components/responses/Unavailable' + x-required-roles: + - EXECUTION_OPERATOR + requestBody: + required: true + content: + application/json: + schema: + $ref: ../schemas/api.schema.json#/$defs/ReconciliationCommand + example: + mode: RESUME + evidence: + reference: CASE-DEMO-001 + reason: Resultado verificado con proveedor. + verifiedActions: + - actionId: buro + result: + score: 71 + consultationId: SIM-001 + providerReference: SIM-001 +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + parameters: + Id: + name: id + in: path + required: true + schema: + type: string + format: uuid + TypeKey: + name: key + in: path + required: true + schema: + type: string + pattern: ^[A-Za-z][A-Za-z0-9_.-]{0,99}$ + IfMatch: + name: If-Match + in: header + required: true + description: Una única revisión fuerte entre comillas; no se admite * ni ETag débil. + schema: + type: string + pattern: ^"[0-9]+"$ + IdempotencyKey: + name: Idempotency-Key + in: header + required: true + schema: + type: string + minLength: 1 + maxLength: 200 + pattern: ^[!-~]+$ + Cursor: + name: cursor + in: query + required: false + schema: + type: string + minLength: 1 + maxLength: 2000 + Limit: + name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 50 + responses: + BadRequest: + description: Sintaxis o cabeceras inválidas. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: Sintaxis o cabeceras inválidas. + status: 400 + code: BAD_REQUEST + correlationId: demo-correlation-001 + Unauthorized: + description: JWT ausente, inválido o vencido. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: JWT ausente, inválido o vencido. + status: 401 + code: UNAUTHORIZED + correlationId: demo-correlation-001 + headers: + WWW-Authenticate: + schema: + type: string + description: Challenge Bearer. + Forbidden: + description: Permiso insuficiente. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: Permiso insuficiente. + status: 403 + code: FORBIDDEN + correlationId: demo-correlation-001 + NotFound: + description: Recurso inexistente o no visible; se oculta existencia ajena. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: Recurso inexistente o no visible; se oculta existencia ajena. + status: 404 + code: NOT_FOUND + correlationId: demo-correlation-001 + Conflict: + description: REQUEST_BUSY, INVALID_EVENT, IDEMPOTENCY_CONFLICT o conflicto de ciclo de vida. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: REQUEST_BUSY, INVALID_EVENT, IDEMPOTENCY_CONFLICT o conflicto de ciclo de vida. + status: 409 + code: REQUEST_BUSY + correlationId: demo-correlation-001 + PreconditionFailed: + description: La revisión If-Match no coincide. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: La revisión If-Match no coincide. + status: 412 + code: REVISION_MISMATCH + correlationId: demo-correlation-001 + PayloadTooLarge: + description: Se excedió el límite de tamaño. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: Se excedió el límite de tamaño. + status: 413 + code: PAYLOAD_TOO_LARGE + correlationId: demo-correlation-001 + UnprocessableEntity: + description: Schema/guardia incumplida o configuración semánticamente inválida. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: Schema/guardia incumplida o configuración semánticamente inválida. + status: 422 + code: VALIDATION_ERROR + correlationId: demo-correlation-001 + PreconditionRequired: + description: Falta If-Match. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: Falta If-Match. + status: 428 + code: PRECONDITION_REQUIRED + correlationId: demo-correlation-001 + Unavailable: + description: Infraestructura no disponible para aceptar la operación. + content: + application/problem+json: + schema: + $ref: ../schemas/api.schema.json#/$defs/Problem + example: + type: about:blank + title: Infraestructura no disponible para aceptar la operación. + status: 503 + code: SERVICE_UNAVAILABLE + correlationId: demo-correlation-001 diff --git a/contracts/validation/README.md b/contracts/validation/README.md new file mode 100644 index 0000000..f4a9a9c --- /dev/null +++ b/contracts/validation/README.md @@ -0,0 +1,8 @@ +# Schema oficial de OpenAPI 3.1 + +openapi-3.1.schema.json es una copia sin modificaciones de: +https://spec.openapis.org/oas/3.1/schema/2022-10-07 + +Descargado el 2026-10-26 para validación sin red en CI. Fuente: OpenAPI Initiative, repositorio https://github.com/OAI/OpenAPI-Specification (licencia Apache 2.0: https://github.com/OAI/OpenAPI-Specification/blob/main/LICENSE). + +Este schema valida el documento OpenAPI, no el contenido de los Schema Objects. ContractValidationTest valida además los archivos JSON Schema contra el meta-schema 2020-12, resuelve todos los $ref locales y valida ejemplos con networknt. No se modifica ni se usa como schema del DSL. diff --git a/contracts/validation/openapi-3.1.schema.json b/contracts/validation/openapi-3.1.schema.json new file mode 100644 index 0000000..468bc7e --- /dev/null +++ b/contracts/validation/openapi-3.1.schema.json @@ -0,0 +1,1440 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/schema/2022-10-07", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "The description of OpenAPI v3.1.x documents without schema validation, as defined by https://spec.openapis.org/oas/v3.1.0", + "type": "object", + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.1\\.\\d+(-.+)?$" + }, + "info": { + "$ref": "#/$defs/info" + }, + "jsonSchemaDialect": { + "type": "string", + "format": "uri", + "default": "https://spec.openapis.org/oas/3.1/dialect/base" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + }, + "default": [ + { + "url": "/" + } + ] + }, + "paths": { + "$ref": "#/$defs/paths" + }, + "webhooks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item-or-reference" + } + }, + "components": { + "$ref": "#/$defs/components" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/$defs/tag" + } + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "openapi", + "info" + ], + "anyOf": [ + { + "required": [ + "paths" + ] + }, + { + "required": [ + "components" + ] + }, + { + "required": [ + "webhooks" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "info": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#info-object", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri" + }, + "contact": { + "$ref": "#/$defs/contact" + }, + "license": { + "$ref": "#/$defs/license" + }, + "version": { + "type": "string" + } + }, + "required": [ + "title", + "version" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "contact": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#contact-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "license": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#license-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name" + ], + "dependentSchemas": { + "identifier": { + "not": { + "required": [ + "url" + ] + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#server-object", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/server-variable" + } + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server-variable": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#server-variable-object", + "type": "object", + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "default" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "components": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#components-object", + "type": "object", + "properties": { + "schemas": { + "type": "object", + "additionalProperties": { + "$dynamicRef": "#meta" + } + }, + "responses": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/response-or-reference" + } + }, + "parameters": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + }, + "requestBodies": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/request-body-or-reference" + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "securitySchemes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/security-scheme-or-reference" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "pathItems": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item-or-reference" + } + } + }, + "patternProperties": { + "^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": { + "$comment": "Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected", + "propertyNames": { + "pattern": "^[a-zA-Z0-9._-]+$" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "paths": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#paths-object", + "type": "object", + "patternProperties": { + "^/": { + "$ref": "#/$defs/path-item" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "path-item": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "get": { + "$ref": "#/$defs/operation" + }, + "put": { + "$ref": "#/$defs/operation" + }, + "post": { + "$ref": "#/$defs/operation" + }, + "delete": { + "$ref": "#/$defs/operation" + }, + "options": { + "$ref": "#/$defs/operation" + }, + "head": { + "$ref": "#/$defs/operation" + }, + "patch": { + "$ref": "#/$defs/operation" + }, + "trace": { + "$ref": "#/$defs/operation" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "path-item-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/path-item" + } + }, + "operation": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "requestBody": { + "$ref": "#/$defs/request-body-or-reference" + }, + "responses": { + "$ref": "#/$defs/responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "external-documentation": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#external-documentation-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameter": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#parameter-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "path", + "cookie" + ] + }, + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "required": [ + "name", + "in" + ], + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "if": { + "properties": { + "in": { + "const": "query" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "allowEmptyValue": { + "default": false, + "type": "boolean" + } + } + }, + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + } + }, + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-path" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-header" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-query" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-form" + } + ], + "$defs": { + "styles-for-path": { + "if": { + "properties": { + "in": { + "const": "path" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "name": { + "pattern": "[^/#?]+$" + }, + "style": { + "default": "simple", + "enum": [ + "matrix", + "label", + "simple" + ] + }, + "required": { + "const": true + } + }, + "required": [ + "required" + ] + } + }, + "styles-for-header": { + "if": { + "properties": { + "in": { + "const": "header" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + } + } + } + }, + "styles-for-query": { + "if": { + "properties": { + "in": { + "const": "query" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "form", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "allowReserved": { + "default": false, + "type": "boolean" + } + } + } + }, + "styles-for-cookie": { + "if": { + "properties": { + "in": { + "const": "cookie" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "form", + "const": "form" + } + } + } + }, + "styles-for-form": { + "if": { + "properties": { + "style": { + "const": "form" + } + }, + "required": [ + "style" + ] + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } + } + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameter-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/parameter" + } + }, + "request-body": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#request-body-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "content": { + "$ref": "#/$defs/content" + }, + "required": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "content" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "request-body-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/request-body" + } + }, + "content": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#fixed-fields-10", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/media-type" + }, + "propertyNames": { + "format": "media-range" + } + }, + "media-type": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#media-type-object", + "type": "object", + "properties": { + "schema": { + "$dynamicRef": "#meta" + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/encoding" + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/examples" + } + ], + "unevaluatedProperties": false + }, + "encoding": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#encoding-object", + "type": "object", + "properties": { + "contentType": { + "type": "string", + "format": "media-range" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "style": { + "default": "form", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "default": false, + "type": "boolean" + } + }, + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/encoding/$defs/explode-default" + } + ], + "unevaluatedProperties": false, + "$defs": { + "explode-default": { + "if": { + "properties": { + "style": { + "const": "form" + } + }, + "required": [ + "style" + ] + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } + } + } + }, + "responses": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#responses-object", + "type": "object", + "properties": { + "default": { + "$ref": "#/$defs/response-or-reference" + } + }, + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": { + "$ref": "#/$defs/response-or-reference" + } + }, + "minProperties": 1, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "response": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#response-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "content": { + "$ref": "#/$defs/content" + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + } + }, + "required": [ + "description" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "response-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/response" + } + }, + "callbacks": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object", + "type": "object", + "$ref": "#/$defs/specification-extensions", + "additionalProperties": { + "$ref": "#/$defs/path-item-or-reference" + } + }, + "callbacks-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/callbacks" + } + }, + "example": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#example-object", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": true, + "externalValue": { + "type": "string", + "format": "uri" + } + }, + "not": { + "required": [ + "value", + "externalValue" + ] + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "example-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/example" + } + }, + "link": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#link-object", + "type": "object", + "properties": { + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/$defs/map-of-strings" + }, + "requestBody": true, + "description": { + "type": "string" + }, + "body": { + "$ref": "#/$defs/server" + } + }, + "oneOf": [ + { + "required": [ + "operationRef" + ] + }, + { + "required": [ + "operationId" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "link-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/link" + } + }, + "header": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#header-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + }, + "explode": { + "default": false, + "type": "boolean" + } + }, + "$ref": "#/$defs/examples" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "header-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/header" + } + }, + "tag": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#tag-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "name" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "reference": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#reference-object", + "type": "object", + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "unevaluatedProperties": false + }, + "schema": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object", + "$dynamicAnchor": "meta", + "type": [ + "object", + "boolean" + ] + }, + "security-scheme": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-scheme-object", + "type": "object", + "properties": { + "type": { + "enum": [ + "apiKey", + "http", + "mutualTLS", + "oauth2", + "openIdConnect" + ] + }, + "description": { + "type": "string" + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-apikey" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http-bearer" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oauth2" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oidc" + } + ], + "unevaluatedProperties": false, + "$defs": { + "type-apikey": { + "if": { + "properties": { + "type": { + "const": "apiKey" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "cookie" + ] + } + }, + "required": [ + "name", + "in" + ] + } + }, + "type-http": { + "if": { + "properties": { + "type": { + "const": "http" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "scheme": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + } + }, + "type-http-bearer": { + "if": { + "properties": { + "type": { + "const": "http" + }, + "scheme": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + }, + "required": [ + "type", + "scheme" + ] + }, + "then": { + "properties": { + "bearerFormat": { + "type": "string" + } + } + } + }, + "type-oauth2": { + "if": { + "properties": { + "type": { + "const": "oauth2" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "flows": { + "$ref": "#/$defs/oauth-flows" + } + }, + "required": [ + "flows" + ] + } + }, + "type-oidc": { + "if": { + "properties": { + "type": { + "const": "openIdConnect" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "openIdConnectUrl": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "openIdConnectUrl" + ] + } + } + } + }, + "security-scheme-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/security-scheme" + } + }, + "oauth-flows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/$defs/oauth-flows/$defs/implicit" + }, + "password": { + "$ref": "#/$defs/oauth-flows/$defs/password" + }, + "clientCredentials": { + "$ref": "#/$defs/oauth-flows/$defs/client-credentials" + }, + "authorizationCode": { + "$ref": "#/$defs/oauth-flows/$defs/authorization-code" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "implicit": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "refreshUrl": { + "type": "string", + "format": "uri" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "password": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "refreshUrl": { + "type": "string", + "format": "uri" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "client-credentials": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "refreshUrl": { + "type": "string", + "format": "uri" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "authorization-code": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "refreshUrl": { + "type": "string", + "format": "uri" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + } + } + }, + "security-requirement": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-requirement-object", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "specification-extensions": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#specification-extensions", + "patternProperties": { + "^x-": true + } + }, + "examples": { + "properties": { + "example": true, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + } + } + }, + "map-of-strings": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/examples/contracts/README.md b/examples/contracts/README.md new file mode 100644 index 0000000..4f7e6c5 --- /dev/null +++ b/examples/contracts/README.md @@ -0,0 +1,13 @@ +# Ejemplos contractuales T02 + +Fixtures de estructura, no flujos de crédito publicables ni resultados reales de proveedores. + +- flows/scoring.json: núcleo de scoring con consentimiento y fronteras 60/70. +- flows/document.json: registro/validación de licencia, INVALID conserva el estado. +- flows/form.json: captura pura sin integraciones, ruta fija y tarea FORM. +- api/: comandos y respuestas REST, simulación y conciliación. +- integrations/: resultados normalizados de demostración, incluidos scores 71/70/61/60. +- invalid/: fixtures que deben ser rechazados; no usarlos para publicar. +- manifest.json: archivo, schema y resultado esperado de todos los fixtures. + +Los tests fallan si un fixture no está registrado o si un ejemplo inválido empieza a aceptarse. También validan todos los ejemplos del OpenAPI. Las fronteras numéricas aquí verifican el contrato de datos, no la decisión N/M/M/MANUAL (T05). Los flujos completos credito-v1, credito-v2-licencia y compra-v1 corresponden a T14. diff --git a/examples/contracts/api/AcceptedEvent.json b/examples/contracts/api/AcceptedEvent.json new file mode 100644 index 0000000..0580002 --- /dev/null +++ b/examples/contracts/api/AcceptedEvent.json @@ -0,0 +1,5 @@ +{ + "executionId": "00000000-0000-0000-0000-000000000789", + "requestId": "00000000-0000-0000-0000-000000000123", + "status": "PENDING" +} diff --git a/examples/contracts/api/ActivateFlow.json b/examples/contracts/api/ActivateFlow.json new file mode 100644 index 0000000..4bdd66d --- /dev/null +++ b/examples/contracts/api/ActivateFlow.json @@ -0,0 +1,3 @@ +{ + "flowId": "00000000-0000-0000-0000-000000000456" +} diff --git a/examples/contracts/api/AvailableEvents.json b/examples/contracts/api/AvailableEvents.json new file mode 100644 index 0000000..1dae401 --- /dev/null +++ b/examples/contracts/api/AvailableEvents.json @@ -0,0 +1,7 @@ +{ + "requestId": "00000000-0000-0000-0000-000000000123", + "revision": 8, + "events": [], + "blocked": [], + "blockedByExecutionId": "00000000-0000-0000-0000-000000000789" +} diff --git a/examples/contracts/api/CreateRequest.json b/examples/contracts/api/CreateRequest.json new file mode 100644 index 0000000..c0279c0 --- /dev/null +++ b/examples/contracts/api/CreateRequest.json @@ -0,0 +1,7 @@ +{ + "typeKey": "CREDITO", + "data": { + "clienteId": "CLI-DEMO", + "consentimientoId": "CONS-DEMO" + } +} diff --git a/examples/contracts/api/EventCommand.json b/examples/contracts/api/EventCommand.json new file mode 100644 index 0000000..2e20cff --- /dev/null +++ b/examples/contracts/api/EventCommand.json @@ -0,0 +1,6 @@ +{ + "event": "REGISTRAR_LICENCIA", + "data": { + "documentId": "00000000-0000-0000-0000-000000000789" + } +} diff --git a/examples/contracts/api/ExecutionView.json b/examples/contracts/api/ExecutionView.json new file mode 100644 index 0000000..5efa164 --- /dev/null +++ b/examples/contracts/api/ExecutionView.json @@ -0,0 +1,16 @@ +{ + "id": "00000000-0000-0000-0000-000000000789", + "requestId": "00000000-0000-0000-0000-000000000123", + "event": "ASIGNAR_CLIENTE", + "status": "SUCCEEDED", + "result": { + "outcome": "CLIENTE_NO_ENCONTRADO", + "fromState": "CREADA", + "toState": "CREADA", + "reasonCodes": [ + "NO_MATCH" + ] + }, + "createdAt": "2026-10-26T17:20:00Z", + "updatedAt": "2026-10-26T17:20:00Z" +} diff --git a/examples/contracts/api/FlowView.json b/examples/contracts/api/FlowView.json new file mode 100644 index 0000000..4197a82 --- /dev/null +++ b/examples/contracts/api/FlowView.json @@ -0,0 +1,150 @@ +{ + "id": "00000000-0000-0000-0000-000000000456", + "status": "DRAFT", + "revision": 0, + "definition": { + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "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": "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": [] + }, + "createdAt": "2026-10-26T17:20:00Z", + "createdBy": "editor-demo" +} diff --git a/examples/contracts/api/HistoryPage.json b/examples/contracts/api/HistoryPage.json new file mode 100644 index 0000000..df06668 --- /dev/null +++ b/examples/contracts/api/HistoryPage.json @@ -0,0 +1,16 @@ +{ + "items": [ + { + "id": "00000000-0000-0000-0000-000000000789", + "requestId": "00000000-0000-0000-0000-000000000123", + "kind": "EVENT_ACCEPTED", + "actorSubject": "analista-demo", + "flowId": "00000000-0000-0000-0000-000000000456", + "fromState": "CREADA", + "detail": { + "transitionId": "asignar-cliente" + }, + "createdAt": "2026-10-26T17:20:00Z" + } + ] +} diff --git a/examples/contracts/api/Problem.json b/examples/contracts/api/Problem.json new file mode 100644 index 0000000..234ecd8 --- /dev/null +++ b/examples/contracts/api/Problem.json @@ -0,0 +1,7 @@ +{ + "type": "about:blank", + "title": "Solicitud ocupada", + "status": 409, + "code": "REQUEST_BUSY", + "correlationId": "demo-correlation-001" +} diff --git a/examples/contracts/api/ReconciliationCommand.json b/examples/contracts/api/ReconciliationCommand.json new file mode 100644 index 0000000..6f464b8 --- /dev/null +++ b/examples/contracts/api/ReconciliationCommand.json @@ -0,0 +1,17 @@ +{ + "mode": "RESUME", + "evidence": { + "reference": "CASE-DEMO-001", + "reason": "Resultado verificado con proveedor." + }, + "verifiedActions": [ + { + "actionId": "buro", + "result": { + "score": 71, + "consultationId": "SIM-001" + }, + "providerReference": "SIM-001" + } + ] +} diff --git a/examples/contracts/api/ReconciliationView.json b/examples/contracts/api/ReconciliationView.json new file mode 100644 index 0000000..76bf357 --- /dev/null +++ b/examples/contracts/api/ReconciliationView.json @@ -0,0 +1,6 @@ +{ + "resolutionId": "00000000-0000-0000-0000-000000000456", + "executionId": "00000000-0000-0000-0000-000000000789", + "status": "PENDING", + "resolvedAt": "2026-10-26T17:20:00Z" +} diff --git a/examples/contracts/api/RequestTypeView.json b/examples/contracts/api/RequestTypeView.json new file mode 100644 index 0000000..3bb9b32 --- /dev/null +++ b/examples/contracts/api/RequestTypeView.json @@ -0,0 +1,6 @@ +{ + "typeKey": "CREDITO", + "activeFlowId": "00000000-0000-0000-0000-000000000456", + "enabled": true, + "revision": 1 +} diff --git a/examples/contracts/api/RequestView.json b/examples/contracts/api/RequestView.json new file mode 100644 index 0000000..494f5ea --- /dev/null +++ b/examples/contracts/api/RequestView.json @@ -0,0 +1,17 @@ +{ + "id": "00000000-0000-0000-0000-000000000123", + "typeKey": "CREDITO", + "flow": { + "id": "00000000-0000-0000-0000-000000000456", + "flowKey": "SCORING_DEMO", + "version": 1 + }, + "currentState": "BURO_PENDIENTE", + "data": { + "clienteId": "CLI-DEMO", + "consentimientoId": "CONS-DEMO" + }, + "revision": 7, + "createdAt": "2026-10-26T17:20:00Z", + "updatedAt": "2026-10-26T17:20:00Z" +} diff --git a/examples/contracts/api/SimulationInput.json b/examples/contracts/api/SimulationInput.json new file mode 100644 index 0000000..2a66ec2 --- /dev/null +++ b/examples/contracts/api/SimulationInput.json @@ -0,0 +1,24 @@ +{ + "state": "BURO_PENDIENTE", + "requestData": { + "clienteId": "CLI-DEMO", + "consentimientoId": "CONS-DEMO" + }, + "event": { + "event": "VALIDAR_BURO", + "data": {} + }, + "actor": { + "subject": "analista-demo", + "roles": [ + "ANALISTA" + ], + "businessScope": "DEMO" + }, + "results": { + "buro": { + "score": 71, + "consultationId": "SIM-001" + } + } +} diff --git a/examples/contracts/api/SimulationResult.json b/examples/contracts/api/SimulationResult.json new file mode 100644 index 0000000..8a00b23 --- /dev/null +++ b/examples/contracts/api/SimulationResult.json @@ -0,0 +1,18 @@ +{ + "transitionId": "consultar-buro", + "outcome": "N", + "toState": "PASO_N", + "data": { + "clienteId": "CLI-DEMO", + "consentimientoId": "CONS-DEMO", + "validaciones": { + "buro": { + "score": 71, + "consultationId": "SIM-001" + } + } + }, + "matchedRuleIds": [ + "alto" + ] +} diff --git a/examples/contracts/api/ValidationReport.json b/examples/contracts/api/ValidationReport.json new file mode 100644 index 0000000..b8b9030 --- /dev/null +++ b/examples/contracts/api/ValidationReport.json @@ -0,0 +1,10 @@ +{ + "valid": false, + "errors": [ + { + "pointer": "/transitions/0/to", + "code": "UNKNOWN_STATE", + "message": "Estado destino inexistente." + } + ] +} diff --git a/examples/contracts/api/available-document.json b/examples/contracts/api/available-document.json new file mode 100644 index 0000000..1f662f8 --- /dev/null +++ b/examples/contracts/api/available-document.json @@ -0,0 +1,34 @@ +{ + "requestId": "00000000-0000-0000-0000-000000000123", + "revision": 3, + "events": [ + { + "event": "REGISTRAR_LICENCIA", + "inputSchema": { + "type": "object", + "required": [ + "documentId" + ], + "properties": { + "documentId": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, + "ui": { + "component": "DOCUMENT_UPLOAD", + "title": "Sube tu licencia", + "documentType": "LICENCIA_CONDUCIR", + "acceptedMimeTypes": [ + "application/pdf", + "image/jpeg", + "image/png" + ], + "maxBytes": 10485760 + } + } + ], + "blocked": [] +} diff --git a/examples/contracts/api/failed-execution.json b/examples/contracts/api/failed-execution.json new file mode 100644 index 0000000..c0474cc --- /dev/null +++ b/examples/contracts/api/failed-execution.json @@ -0,0 +1,11 @@ +{ + "id": "00000000-0000-0000-0000-000000000789", + "requestId": "00000000-0000-0000-0000-000000000123", + "event": "VALIDAR_BURO", + "status": "FAILED", + "error": { + "code": "PROVIDER_CONTRACT_ERROR" + }, + "createdAt": "2026-10-26T17:20:00Z", + "updatedAt": "2026-10-26T17:20:00Z" +} diff --git a/examples/contracts/api/reconcile-close.json b/examples/contracts/api/reconcile-close.json new file mode 100644 index 0000000..2f05f76 --- /dev/null +++ b/examples/contracts/api/reconcile-close.json @@ -0,0 +1,8 @@ +{ + "mode": "CLOSE_WITHOUT_TRANSITION", + "evidence": { + "reference": "CASE-002", + "reason": "Proveedor confirmó ausencia de efectos." + }, + "effectStatus": "ABSENT_VERIFIED" +} diff --git a/examples/contracts/flows/document.json b/examples/contracts/flows/document.json new file mode 100644 index 0000000..ee9b7eb --- /dev/null +++ b/examples/contracts/flows/document.json @@ -0,0 +1,149 @@ +{ + "schemaVersion": 1, + "flowKey": "DOCUMENT_DEMO", + "version": 1, + "initialState": "LICENCIA_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "documentos": { + "type": "object" + } + }, + "additionalProperties": false + }, + "initialDataSchema": { + "type": "object", + "additionalProperties": false + }, + "states": [ + { + "key": "LICENCIA_PENDIENTE", + "terminal": false + }, + { + "key": "COMPLETA", + "terminal": true + } + ], + "events": [ + { + "key": "REGISTRAR_LICENCIA", + "inputSchema": { + "type": "object", + "required": [ + "documentId" + ], + "properties": { + "documentId": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "licencia", + "from": "LICENCIA_PENDIENTE", + "event": "REGISTRAR_LICENCIA", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "registrar", + "type": "REGISTER_DOCUMENT", + "documentType": "LICENCIA_CONDUCIR", + "inputs": { + "documentId": { + "path": "/event/data/documentId" + } + }, + "resultKey": "documento" + }, + { + "id": "validar", + "type": "INVOKE_INTEGRATION", + "integrationKey": "DOCUMENTO_VALIDA", + "contractVersion": "1", + "profile": "LICENCIA_CONDUCIR", + "profileVersion": "1", + "inputs": { + "documentId": { + "path": "/results/documento/documentId" + } + }, + "resultKey": "validacion" + } + ], + "decision": "DOCUMENTO_VALIDO", + "destinations": { + "VALID": "COMPLETA", + "INVALID": "LICENCIA_PENDIENTE" + }, + "updates": [ + { + "target": "/documentos/licencia", + "value": { + "path": "/results/validacion" + } + } + ] + } + ], + "decisions": [ + { + "id": "DOCUMENTO_VALIDO", + "hitPolicy": "UNIQUE", + "rules": [ + { + "id": "valida", + "when": { + "op": "EQ", + "left": { + "path": "/results/validacion/status" + }, + "right": { + "literal": "VALID" + } + }, + "output": "VALID" + } + ], + "defaultOutput": "INVALID" + } + ], + "tasks": [ + { + "state": "LICENCIA_PENDIENTE", + "event": "REGISTRAR_LICENCIA", + "inputSchema": { + "type": "object", + "required": [ + "documentId" + ], + "properties": { + "documentId": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, + "ui": { + "component": "DOCUMENT_UPLOAD", + "title": "Sube tu licencia", + "documentType": "LICENCIA_CONDUCIR", + "acceptedMimeTypes": [ + "application/pdf", + "image/jpeg", + "image/png" + ], + "maxBytes": 10485760 + } + } + ] +} diff --git a/examples/contracts/flows/form.json b/examples/contracts/flows/form.json new file mode 100644 index 0000000..f2f9004 --- /dev/null +++ b/examples/contracts/flows/form.json @@ -0,0 +1,95 @@ +{ + "schemaVersion": 1, + "flowKey": "FORM_DEMO", + "version": 1, + "initialState": "CAPTURA", + "dataSchema": { + "type": "object", + "properties": { + "referencia": { + "type": "string" + } + }, + "additionalProperties": false + }, + "initialDataSchema": { + "type": "object", + "additionalProperties": false + }, + "states": [ + { + "key": "CAPTURA", + "terminal": false + }, + { + "key": "COMPLETA", + "terminal": true + } + ], + "events": [ + { + "key": "CAPTURAR", + "inputSchema": { + "type": "object", + "required": [ + "referencia" + ], + "properties": { + "referencia": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "capturar", + "from": "CAPTURA", + "event": "CAPTURAR", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [], + "to": "COMPLETA", + "outcome": "CAPTURADO", + "updates": [ + { + "target": "/referencia", + "value": { + "path": "/event/data/referencia" + } + } + ] + } + ], + "decisions": [], + "tasks": [ + { + "state": "CAPTURA", + "event": "CAPTURAR", + "inputSchema": { + "type": "object", + "required": [ + "referencia" + ], + "properties": { + "referencia": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "ui": { + "component": "FORM", + "title": "Capturar referencia", + "fieldOrder": [ + "referencia" + ] + } + } + ] +} diff --git a/examples/contracts/flows/scoring.json b/examples/contracts/flows/scoring.json new file mode 100644 index 0000000..ba5e8c4 --- /dev/null +++ b/examples/contracts/flows/scoring.json @@ -0,0 +1,143 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "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": "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": [] +} diff --git a/examples/contracts/integrations/bureau-60.json b/examples/contracts/integrations/bureau-60.json new file mode 100644 index 0000000..cfbe89e --- /dev/null +++ b/examples/contracts/integrations/bureau-60.json @@ -0,0 +1,4 @@ +{ + "score": 60, + "consultationId": "SIM-60" +} diff --git a/examples/contracts/integrations/bureau-61.json b/examples/contracts/integrations/bureau-61.json new file mode 100644 index 0000000..2613095 --- /dev/null +++ b/examples/contracts/integrations/bureau-61.json @@ -0,0 +1,4 @@ +{ + "score": 61, + "consultationId": "SIM-61" +} diff --git a/examples/contracts/integrations/bureau-70.json b/examples/contracts/integrations/bureau-70.json new file mode 100644 index 0000000..f2fee08 --- /dev/null +++ b/examples/contracts/integrations/bureau-70.json @@ -0,0 +1,4 @@ +{ + "score": 70, + "consultationId": "SIM-70" +} diff --git a/examples/contracts/integrations/bureau-71.json b/examples/contracts/integrations/bureau-71.json new file mode 100644 index 0000000..97bab73 --- /dev/null +++ b/examples/contracts/integrations/bureau-71.json @@ -0,0 +1,4 @@ +{ + "score": 71, + "consultationId": "SIM-71" +} diff --git a/examples/contracts/integrations/customer-found.json b/examples/contracts/integrations/customer-found.json new file mode 100644 index 0000000..b5773ba --- /dev/null +++ b/examples/contracts/integrations/customer-found.json @@ -0,0 +1,4 @@ +{ + "found": true, + "customerId": "CLI-DEMO" +} diff --git a/examples/contracts/integrations/customer-missing.json b/examples/contracts/integrations/customer-missing.json new file mode 100644 index 0000000..668beb9 --- /dev/null +++ b/examples/contracts/integrations/customer-missing.json @@ -0,0 +1,3 @@ +{ + "found": false +} diff --git a/examples/contracts/integrations/document-invalid.json b/examples/contracts/integrations/document-invalid.json new file mode 100644 index 0000000..ff22b21 --- /dev/null +++ b/examples/contracts/integrations/document-invalid.json @@ -0,0 +1,6 @@ +{ + "status": "INVALID", + "reasonCodes": [ + "UNREADABLE" + ] +} diff --git a/examples/contracts/invalid/ambiguous-routing-shape.json b/examples/contracts/invalid/ambiguous-routing-shape.json new file mode 100644 index 0000000..c38508f --- /dev/null +++ b/examples/contracts/invalid/ambiguous-routing-shape.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "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" + } + } + ], + "to": "PASO_N", + "outcome": "N" + } + ], + "decisions": [ + { + "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": [] +} diff --git a/examples/contracts/invalid/arbitrary-url.json b/examples/contracts/invalid/arbitrary-url.json new file mode 100644 index 0000000..d0b45b6 --- /dev/null +++ b/examples/contracts/invalid/arbitrary-url.json @@ -0,0 +1,144 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "consentimientoId": { + "path": "/request/data/consentimientoId" + } + }, + "resultKey": "buro", + "url": "https://untrusted.example" + } + ], + "decision": "RUTA_SCORING", + "destinations": { + "N": "PASO_N", + "M": "PASO_M", + "MANUAL": "REVISION_MANUAL" + }, + "updates": [ + { + "target": "/validaciones/buro", + "value": { + "path": "/results/buro" + } + } + ] + } + ], + "decisions": [ + { + "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": [] +} diff --git a/examples/contracts/invalid/busy-with-events.json b/examples/contracts/invalid/busy-with-events.json new file mode 100644 index 0000000..7081700 --- /dev/null +++ b/examples/contracts/invalid/busy-with-events.json @@ -0,0 +1,12 @@ +{ + "requestId": "00000000-0000-0000-0000-000000000123", + "revision": 3, + "events": [ + { + "event": "CAPTURAR", + "inputSchema": {} + } + ], + "blocked": [], + "blockedByExecutionId": "00000000-0000-0000-0000-000000000789" +} diff --git a/examples/contracts/invalid/create-forged-owner.json b/examples/contracts/invalid/create-forged-owner.json new file mode 100644 index 0000000..3357b58 --- /dev/null +++ b/examples/contracts/invalid/create-forged-owner.json @@ -0,0 +1,5 @@ +{ + "typeKey": "CREDITO", + "data": {}, + "ownerSubject": "otro" +} diff --git a/examples/contracts/invalid/customer-without-id.json b/examples/contracts/invalid/customer-without-id.json new file mode 100644 index 0000000..83bea40 --- /dev/null +++ b/examples/contracts/invalid/customer-without-id.json @@ -0,0 +1,3 @@ +{ + "found": true +} diff --git a/examples/contracts/invalid/document-unknown-status.json b/examples/contracts/invalid/document-unknown-status.json new file mode 100644 index 0000000..609dac3 --- /dev/null +++ b/examples/contracts/invalid/document-unknown-status.json @@ -0,0 +1,4 @@ +{ + "status": "ERROR", + "reasonCodes": [] +} diff --git a/examples/contracts/invalid/empty-evidence.json b/examples/contracts/invalid/empty-evidence.json new file mode 100644 index 0000000..117d0d7 --- /dev/null +++ b/examples/contracts/invalid/empty-evidence.json @@ -0,0 +1,8 @@ +{ + "mode": "CLOSE_WITHOUT_TRANSITION", + "evidence": { + "reference": "", + "reason": "" + }, + "effectStatus": "ABSENT_VERIFIED" +} diff --git a/examples/contracts/invalid/event-forged-state.json b/examples/contracts/invalid/event-forged-state.json new file mode 100644 index 0000000..f2b1a8f --- /dev/null +++ b/examples/contracts/invalid/event-forged-state.json @@ -0,0 +1,5 @@ +{ + "event": "CAPTURAR", + "data": {}, + "currentState": "APROBADA" +} diff --git a/examples/contracts/invalid/external-schema-ref.json b/examples/contracts/invalid/external-schema-ref.json new file mode 100644 index 0000000..d270a3a --- /dev/null +++ b/examples/contracts/invalid/external-schema-ref.json @@ -0,0 +1,142 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "$ref": "https://untrusted.example/schema" + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "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": "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": [] +} diff --git a/examples/contracts/invalid/invalid-pointer.json b/examples/contracts/invalid/invalid-pointer.json new file mode 100644 index 0000000..61bef1a --- /dev/null +++ b/examples/contracts/invalid/invalid-pointer.json @@ -0,0 +1,143 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "consentimientoId": { + "path": "/request/data/consentimientoId" + } + }, + "resultKey": "buro" + } + ], + "decision": "RUTA_SCORING", + "destinations": { + "N": "PASO_N", + "M": "PASO_M", + "MANUAL": "REVISION_MANUAL" + }, + "updates": [ + { + "target": "/a~2b", + "value": { + "path": "/results/buro" + } + } + ] + } + ], + "decisions": [ + { + "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": [] +} diff --git a/examples/contracts/invalid/invalid-uuid.json b/examples/contracts/invalid/invalid-uuid.json new file mode 100644 index 0000000..e1840a0 --- /dev/null +++ b/examples/contracts/invalid/invalid-uuid.json @@ -0,0 +1,5 @@ +{ + "executionId": "incorrecto", + "requestId": "00000000-0000-0000-0000-000000000123", + "status": "PENDING" +} diff --git a/examples/contracts/invalid/malformed-schema.json b/examples/contracts/invalid/malformed-schema.json new file mode 100644 index 0000000..a0da835 --- /dev/null +++ b/examples/contracts/invalid/malformed-schema.json @@ -0,0 +1,142 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "numeric" + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "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": "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": [] +} diff --git a/examples/contracts/invalid/manual-state-resolution.json b/examples/contracts/invalid/manual-state-resolution.json new file mode 100644 index 0000000..0bb4c2c --- /dev/null +++ b/examples/contracts/invalid/manual-state-resolution.json @@ -0,0 +1,14 @@ +{ + "mode": "RESUME", + "evidence": { + "reference": "CASE-001", + "reason": "verificado" + }, + "verifiedActions": [ + { + "actionId": "buro", + "result": {} + } + ], + "toState": "APROBADA" +} diff --git a/examples/contracts/invalid/missing-default.json b/examples/contracts/invalid/missing-default.json new file mode 100644 index 0000000..4b79554 --- /dev/null +++ b/examples/contracts/invalid/missing-default.json @@ -0,0 +1,142 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "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": "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" + } + ] + } + ], + "tasks": [] +} diff --git a/examples/contracts/invalid/missing-score.json b/examples/contracts/invalid/missing-score.json new file mode 100644 index 0000000..f2b2387 --- /dev/null +++ b/examples/contracts/invalid/missing-score.json @@ -0,0 +1,3 @@ +{ + "consultationId": "SIM-001" +} diff --git a/examples/contracts/invalid/protected-context-read.json b/examples/contracts/invalid/protected-context-read.json new file mode 100644 index 0000000..5a09442 --- /dev/null +++ b/examples/contracts/invalid/protected-context-read.json @@ -0,0 +1,143 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/ownerSubject" + }, + "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": "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": [] +} diff --git a/examples/contracts/invalid/retry-unbounded.json b/examples/contracts/invalid/retry-unbounded.json new file mode 100644 index 0000000..ca5f8e8 --- /dev/null +++ b/examples/contracts/invalid/retry-unbounded.json @@ -0,0 +1,146 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "consentimientoId": { + "path": "/request/data/consentimientoId" + } + }, + "resultKey": "buro", + "retry": { + "maxAttempts": 0 + } + } + ], + "decision": "RUTA_SCORING", + "destinations": { + "N": "PASO_N", + "M": "PASO_M", + "MANUAL": "REVISION_MANUAL" + }, + "updates": [ + { + "target": "/validaciones/buro", + "value": { + "path": "/results/buro" + } + } + ] + } + ], + "decisions": [ + { + "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": [] +} diff --git a/examples/contracts/invalid/script-condition.json b/examples/contracts/invalid/script-condition.json new file mode 100644 index 0000000..70c8328 --- /dev/null +++ b/examples/contracts/invalid/script-condition.json @@ -0,0 +1,138 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "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": "RUTA_SCORING", + "hitPolicy": "UNIQUE", + "rules": [ + { + "id": "alto", + "when": { + "op": "SPEL", + "expression": "true" + }, + "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": [] +} diff --git a/examples/contracts/invalid/string-score.json b/examples/contracts/invalid/string-score.json new file mode 100644 index 0000000..2c05c04 --- /dev/null +++ b/examples/contracts/invalid/string-score.json @@ -0,0 +1,4 @@ +{ + "score": "71", + "consultationId": "SIM-001" +} diff --git a/examples/contracts/invalid/success-without-outcome.json b/examples/contracts/invalid/success-without-outcome.json new file mode 100644 index 0000000..0edede0 --- /dev/null +++ b/examples/contracts/invalid/success-without-outcome.json @@ -0,0 +1,8 @@ +{ + "id": "00000000-0000-0000-0000-000000000789", + "requestId": "00000000-0000-0000-0000-000000000123", + "event": "VALIDAR_BURO", + "status": "SUCCEEDED", + "createdAt": "2026-10-26T17:20:00Z", + "updatedAt": "2026-10-26T17:20:00Z" +} diff --git a/examples/contracts/invalid/unknown-handler.json b/examples/contracts/invalid/unknown-handler.json new file mode 100644 index 0000000..4afd4e9 --- /dev/null +++ b/examples/contracts/invalid/unknown-handler.json @@ -0,0 +1,143 @@ +{ + "schemaVersion": 1, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "EXECUTE_SCRIPT", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "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": "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": [] +} diff --git a/examples/contracts/invalid/unsupported-dsl.json b/examples/contracts/invalid/unsupported-dsl.json new file mode 100644 index 0000000..2ac40e3 --- /dev/null +++ b/examples/contracts/invalid/unsupported-dsl.json @@ -0,0 +1,143 @@ +{ + "schemaVersion": 2, + "flowKey": "SCORING_DEMO", + "version": 1, + "initialState": "BURO_PENDIENTE", + "dataSchema": { + "type": "object", + "properties": { + "clienteId": { + "type": "string" + }, + "consentimientoId": { + "type": "string" + }, + "validaciones": { + "type": "object" + } + }, + "required": [ + "clienteId", + "consentimientoId" + ], + "additionalProperties": false + }, + "states": [ + { + "key": "BURO_PENDIENTE", + "terminal": false + }, + { + "key": "PASO_N", + "terminal": true + }, + { + "key": "PASO_M", + "terminal": true + }, + { + "key": "REVISION_MANUAL", + "terminal": true + } + ], + "events": [ + { + "key": "VALIDAR_BURO", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + } + ], + "transitions": [ + { + "id": "consultar-buro", + "from": "BURO_PENDIENTE", + "event": "VALIDAR_BURO", + "allowedRoles": [ + "ANALISTA" + ], + "actions": [ + { + "id": "buro", + "type": "INVOKE_INTEGRATION", + "integrationKey": "BURO_CONSULTA", + "contractVersion": "1", + "inputs": { + "clienteId": { + "path": "/request/data/clienteId" + }, + "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": "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": [] +} diff --git a/examples/contracts/invalid/valid-report-with-errors.json b/examples/contracts/invalid/valid-report-with-errors.json new file mode 100644 index 0000000..f7bbdfc --- /dev/null +++ b/examples/contracts/invalid/valid-report-with-errors.json @@ -0,0 +1,10 @@ +{ + "valid": true, + "errors": [ + { + "pointer": "", + "code": "ERROR", + "message": "Invalid" + } + ] +} diff --git a/examples/contracts/manifest.json b/examples/contracts/manifest.json new file mode 100644 index 0000000..7be7fe6 --- /dev/null +++ b/examples/contracts/manifest.json @@ -0,0 +1,262 @@ +[ + { + "file": "api/CreateRequest.json", + "schema": "api.schema.json#/$defs/CreateRequest", + "valid": true + }, + { + "file": "api/EventCommand.json", + "schema": "api.schema.json#/$defs/EventCommand", + "valid": true + }, + { + "file": "api/RequestView.json", + "schema": "api.schema.json#/$defs/RequestView", + "valid": true + }, + { + "file": "api/AcceptedEvent.json", + "schema": "api.schema.json#/$defs/AcceptedEvent", + "valid": true + }, + { + "file": "api/ExecutionView.json", + "schema": "api.schema.json#/$defs/ExecutionView", + "valid": true + }, + { + "file": "api/AvailableEvents.json", + "schema": "api.schema.json#/$defs/AvailableEvents", + "valid": true + }, + { + "file": "api/HistoryPage.json", + "schema": "api.schema.json#/$defs/HistoryPage", + "valid": true + }, + { + "file": "api/FlowView.json", + "schema": "api.schema.json#/$defs/FlowView", + "valid": true + }, + { + "file": "api/ValidationReport.json", + "schema": "api.schema.json#/$defs/ValidationReport", + "valid": true + }, + { + "file": "api/ActivateFlow.json", + "schema": "api.schema.json#/$defs/ActivateFlow", + "valid": true + }, + { + "file": "api/RequestTypeView.json", + "schema": "api.schema.json#/$defs/RequestTypeView", + "valid": true + }, + { + "file": "api/SimulationInput.json", + "schema": "api.schema.json#/$defs/SimulationInput", + "valid": true + }, + { + "file": "api/SimulationResult.json", + "schema": "api.schema.json#/$defs/SimulationResult", + "valid": true + }, + { + "file": "api/ReconciliationCommand.json", + "schema": "api.schema.json#/$defs/ReconciliationCommand", + "valid": true + }, + { + "file": "api/ReconciliationView.json", + "schema": "api.schema.json#/$defs/ReconciliationView", + "valid": true + }, + { + "file": "api/Problem.json", + "schema": "api.schema.json#/$defs/Problem", + "valid": true + }, + { + "file": "flows/scoring.json", + "schema": "flow-definition.schema.json", + "valid": true + }, + { + "file": "flows/document.json", + "schema": "flow-definition.schema.json", + "valid": true + }, + { + "file": "flows/form.json", + "schema": "flow-definition.schema.json", + "valid": true + }, + { + "file": "api/available-document.json", + "schema": "api.schema.json#/$defs/AvailableEvents", + "valid": true + }, + { + "file": "api/reconcile-close.json", + "schema": "api.schema.json#/$defs/ReconciliationCommand", + "valid": true + }, + { + "file": "api/failed-execution.json", + "schema": "api.schema.json#/$defs/ExecutionView", + "valid": true + }, + { + "file": "integrations/bureau-71.json", + "schema": "integration-results.schema.json#/$defs/BureauResult", + "valid": true + }, + { + "file": "integrations/bureau-70.json", + "schema": "integration-results.schema.json#/$defs/BureauResult", + "valid": true + }, + { + "file": "integrations/bureau-61.json", + "schema": "integration-results.schema.json#/$defs/BureauResult", + "valid": true + }, + { + "file": "integrations/bureau-60.json", + "schema": "integration-results.schema.json#/$defs/BureauResult", + "valid": true + }, + { + "file": "integrations/customer-found.json", + "schema": "integration-results.schema.json#/$defs/CustomerResult", + "valid": true + }, + { + "file": "integrations/customer-missing.json", + "schema": "integration-results.schema.json#/$defs/CustomerResult", + "valid": true + }, + { + "file": "integrations/document-invalid.json", + "schema": "integration-results.schema.json#/$defs/DocumentValidationResult", + "valid": true + }, + { + "file": "invalid/missing-score.json", + "schema": "integration-results.schema.json#/$defs/BureauResult", + "valid": false + }, + { + "file": "invalid/string-score.json", + "schema": "integration-results.schema.json#/$defs/BureauResult", + "valid": false + }, + { + "file": "invalid/customer-without-id.json", + "schema": "integration-results.schema.json#/$defs/CustomerResult", + "valid": false + }, + { + "file": "invalid/document-unknown-status.json", + "schema": "integration-results.schema.json#/$defs/DocumentValidationResult", + "valid": false + }, + { + "file": "invalid/event-forged-state.json", + "schema": "api.schema.json#/$defs/EventCommand", + "valid": false + }, + { + "file": "invalid/create-forged-owner.json", + "schema": "api.schema.json#/$defs/CreateRequest", + "valid": false + }, + { + "file": "invalid/manual-state-resolution.json", + "schema": "api.schema.json#/$defs/ReconciliationCommand", + "valid": false + }, + { + "file": "invalid/empty-evidence.json", + "schema": "api.schema.json#/$defs/ReconciliationCommand", + "valid": false + }, + { + "file": "invalid/busy-with-events.json", + "schema": "api.schema.json#/$defs/AvailableEvents", + "valid": false + }, + { + "file": "invalid/success-without-outcome.json", + "schema": "api.schema.json#/$defs/ExecutionView", + "valid": false + }, + { + "file": "invalid/invalid-uuid.json", + "schema": "api.schema.json#/$defs/AcceptedEvent", + "valid": false + }, + { + "file": "invalid/valid-report-with-errors.json", + "schema": "api.schema.json#/$defs/ValidationReport", + "valid": false + }, + { + "file": "invalid/unsupported-dsl.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/arbitrary-url.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/unknown-handler.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/script-condition.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/missing-default.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/ambiguous-routing-shape.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/invalid-pointer.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/protected-context-read.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/external-schema-ref.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/malformed-schema.json", + "schema": "flow-definition.schema.json", + "valid": false + }, + { + "file": "invalid/retry-unbounded.json", + "schema": "flow-definition.schema.json", + "valid": false + } +] diff --git a/examples/http/contracts.http b/examples/http/contracts.http new file mode 100644 index 0000000..f7782aa --- /dev/null +++ b/examples/http/contracts.http @@ -0,0 +1,78 @@ +# Contratos objetivo T02: estas rutas aún no están implementadas. +# Configurar token y reemplazar IDs/ETags con respuestas reales cuando estén disponibles. +@base = http://localhost:8080/api/v1 +@flowId = 00000000-0000-0000-0000-000000000456 +@requestId = 00000000-0000-0000-0000-000000000123 +@executionId = 00000000-0000-0000-0000-000000000789 + +### Crear borrador (FLOW_EDITOR); fixture contractual, no flujo completo T14. +POST {{base}}/admin/flows +Authorization: Bearer {{token}} +Content-Type: application/json + +< ../contracts/flows/scoring.json + +### Validar; valid=false se reporta con HTTP 200 y errores JSON Pointer. +POST {{base}}/admin/flows/{{flowId}}/validate +Authorization: Bearer {{token}} + +### Simular sin invocar proveedores. +POST {{base}}/admin/flows/{{flowId}}/simulate +Authorization: Bearer {{token}} +Content-Type: application/json + +< ../contracts/api/SimulationInput.json + +### Publicar: usar ETag del borrador real, no el valor ilustrativo. +POST {{base}}/admin/flows/{{flowId}}/publish +Authorization: Bearer {{token}} +If-Match: "0" + +### Activar: el tipo CREDITO debe existir; If-Match corresponde al tipo. +PUT {{base}}/admin/request-types/CREDITO/active-flow +Authorization: Bearer {{token}} +If-Match: "0" +Content-Type: application/json + +{"flowId":"{{flowId}}"} + +### Crear solicitud. Repetir la misma clave/comando retorna el mismo recurso. +POST {{base}}/requests +Authorization: Bearer {{token}} +Idempotency-Key: demo-create-001 +Content-Type: application/json + +< ../contracts/api/CreateRequest.json + +### Consultar solicitud y obtener ETag. +GET {{base}}/requests/{{requestId}} +Authorization: Bearer {{token}} + +### Obtener tareas disponibles. +GET {{base}}/requests/{{requestId}}/available-events +Authorization: Bearer {{token}} + +### Aceptar evento; body sin resultados de proveedor. +POST {{base}}/requests/{{requestId}}/events +Authorization: Bearer {{token}} +Idempotency-Key: demo-bureau-001 +If-Match: "0" +Content-Type: application/json + +{"event":"VALIDAR_BURO","data":{}} + +### Consultar ejecución indicada por Location. +GET {{base}}/executions/{{executionId}} +Authorization: Bearer {{token}} + +### Historial paginado. +GET {{base}}/requests/{{requestId}}/history?limit=50&kind=EVENT_ACCEPTED +Authorization: Bearer {{token}} + +### Conciliar sólo RECONCILIATION_REQUIRED y con evidencia realmente verificada. +POST {{base}}/admin/executions/{{executionId}}/resolve +Authorization: Bearer {{token}} +Idempotency-Key: demo-resolution-001 +Content-Type: application/json + +< ../contracts/api/ReconciliationCommand.json diff --git a/pom.xml b/pom.xml index 4612928..aba2f26 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,8 @@ org.flywaydbflyway-database-postgresql org.postgresqlpostgresqlruntime org.junit.jupiterjunit-jupitertest + com.networkntjson-schema-validator1.5.9test + io.swagger.parser.v3swagger-parser-v32.1.35test @@ -51,4 +53,29 @@ + + + deployment-smoke + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/*IT.java + **/IT*.java + **/*ITCase.java + **/*SmokeCheck.java + + + ${smoke.baseUrl} + + true + + + + + + diff --git a/schemas/README.md b/schemas/README.md index e2154f6..005e6de 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -1,5 +1,42 @@ -# Schemas pendientes de T02 +# JSON Schema del DSL y DTOs -Crear flow-definition.schema.json y schemas de entrada/salida, datos y tareas. -Separar schemaVersion de la versión del flujo. Validar AST tipado y límites. -Una configuración válida estructuralmente aún requiere la validación semántica T06. +Dialect: JSON Schema 2020-12. Archivos canónicos editables directamente; no se requiere generador. + +| Archivo | Contenido | +|---|---| +| flow-definition.schema.json | Definición completa de flujo y $defs para estados, eventos, valores, condiciones, acciones, decisiones, transiciones y tareas | +| api.schema.json | $defs de comandos, consultas, administración, errores, simulación y conciliación REST | +| integration-results.schema.json | Ejemplos de contratos normalizados de cliente, buró y documentos; pendientes de confirmar con proveedores en T11 | + +## DSL v1 + +schemaVersion versiona el formato; version versiona el proceso. Keys/IDs: 1–100 caracteres, comienzan por letra y permiten letras ASCII, dígitos, punto, guion y guion bajo. Los estados/eventos siguen siendo strings configurables, no enums del negocio. + +`dataSchema` describe datos persistidos y `initialDataSchema` restringe entrada inicial. Son opcionales estructuralmente para aceptar borradores y el ejemplo mínimo original; T06 deberá exigir dataSchema de objeto al publicar. Si initialDataSchema falta, usar dataSchema; al crear validar ambos cuando están presentes. En los schemas de entrada conviene additionalProperties=false y sólo campos aceptados del cliente. La publicación debe rechazar configuraciones que acepten resultados de proveedor como autoridad del front. + +`InlineSchema` admite booleanos y un subconjunto explícito de JSON Schema: type, properties, required, additionalProperties, items, enum/const, longitudes/pattern/format, rangos numéricos, límites de arrays/objetos y allOf/anyOf/oneOf/not/if/then/else. Formats: uuid, date-time, date, email, uri. No admite referencias externas, $ref, $id ni vocabularios ejecutables. La validación de formats está activada en las pruebas. Schemas embebidos también se validan con el meta-schema 2020-12; consistencia entre rangos y compatibilidad de mappings corresponden a T06. + +Valores: exactamente `{ "path": "/results/buro/score" }` o `{ "literal": 70 }`. Contextos permitidos: /request/data, /event/data, /results y /actor. Paths usan JSON Pointer y escapes ~0/~1; no son expresiones ejecutables. + +Condiciones: + +- EQ, NEQ, GT, GTE, LT, LTE, IN: op, left y right. +- EXISTS: op y value con path (no literal). +- 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. + +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. + +`updates.target` es un JSON Pointer no vacío relativo al contenido de request.data. `/validaciones/buro` actualiza ese campo dentro de data; nunca apunta a identidad, estado, revision o ACL de la solicitud. No se puede reemplazar la raíz. T06 rechazará destinos solapados/duplicados y comprobará que el resultado cumple dataSchema antes del commit. + +Actions del MVP: REGISTER_DOCUMENT, INVOKE_INTEGRATION y NO_OP (sólo simulación, no publicable). No admiten URL, headers, secretos, clases Java ni clasificación de efectos enviada por el editor. Catálogo de despliegue aporta estas capacidades. ContractVersion/profileVersion pueden omitirse en borrador y deben resolverse/fijarse al publicar. Un perfil desconocido se rechaza semánticamente. + +Tareas: estado/evento/inputSchema/ui. DOCUMENT_UPLOAD exige documentType, acceptedMimeTypes y maxBytes (techo del DSL 100 MiB, sujeto a un límite operativo menor); FORM permite fieldOrder. La UI no sustituye restricciones backend. T06 debe exigir equivalencia del inputSchema de tarea y evento y compatibilidad del documentType con REGISTER_DOCUMENT. El servicio documental verifica MIME real, tamaño, pertenencia, checksum y antimalware en T13. + +## Límites y fronteras + +El schema aplica 200 estados, 500 transiciones y 50 acciones por transición; limita también colecciones auxiliares. Longitud en bytes de definición (1 MiB), datos (256 KiB), profundidad AST 20, total 1.000 nodos por decisión, unicidad por ID, alcanzabilidad, catálogos, roles y destinos son comprobaciones semánticas/de recepción pendientes de T06/T09. No confundir arrays únicos con unicidad por clave ni límites de JSON Schema con protección frente a cargas antes de deserializar. + +BureauResult exige score numérico y consultationId; no asume rango universal 0–100. DocumentValidationResult sólo admite VALID/INVALID: fallos técnicos no se convierten a INVALID. CustomerResult exige customerId cuando found=true. Son ejemplos de contrato interno, no conexión real a proveedores. diff --git a/schemas/api.schema.json b/schemas/api.schema.json new file mode 100644 index 0000000..ddc3dd3 --- /dev/null +++ b/schemas/api.schema.json @@ -0,0 +1,927 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "REST DTO contracts v1", + "$defs": { + "Key": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "Revision": { + "type": "integer", + "minimum": 0 + }, + "Data": { + "type": "object" + }, + "UtcTimestamp": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + }, + "ExecutionStatus": { + "type": "string", + "enum": [ + "PENDING", + "RUNNING", + "RETRY_WAIT", + "SUCCEEDED", + "FAILED", + "RECONCILIATION_REQUIRED" + ] + }, + "FlowRef": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "flowKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "version": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "id", + "flowKey", + "version" + ], + "additionalProperties": false + }, + "CreateRequest": { + "type": "object", + "properties": { + "typeKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "data": { + "$ref": "#/$defs/Data" + } + }, + "required": [ + "typeKey", + "data" + ], + "additionalProperties": false + }, + "EventCommand": { + "type": "object", + "properties": { + "event": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "data": { + "$ref": "#/$defs/Data" + } + }, + "required": [ + "event", + "data" + ], + "additionalProperties": false + }, + "RequestView": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "typeKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "flow": { + "$ref": "#/$defs/FlowRef" + }, + "currentState": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "data": { + "$ref": "#/$defs/Data" + }, + "revision": { + "$ref": "#/$defs/Revision" + }, + "activeExecutionId": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + } + }, + "required": [ + "id", + "typeKey", + "flow", + "currentState", + "data", + "revision", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + }, + "AcceptedEvent": { + "type": "object", + "properties": { + "executionId": { + "type": "string", + "format": "uuid" + }, + "requestId": { + "type": "string", + "format": "uuid" + }, + "status": { + "$ref": "#/$defs/ExecutionStatus" + } + }, + "required": [ + "executionId", + "requestId", + "status" + ], + "additionalProperties": false + }, + "BusinessResult": { + "type": "object", + "properties": { + "outcome": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "fromState": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "toState": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "reasonCodes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "minItems": 0, + "maxItems": 100 + } + }, + "required": [ + "outcome", + "fromState", + "toState" + ], + "additionalProperties": false + }, + "ExecutionError": { + "type": "object", + "properties": { + "code": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "message": { + "type": "string", + "maxLength": 2000 + } + }, + "required": [ + "code" + ], + "additionalProperties": false + }, + "ExecutionView": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "requestId": { + "type": "string", + "format": "uuid" + }, + "event": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "status": { + "$ref": "#/$defs/ExecutionStatus" + }, + "result": { + "$ref": "#/$defs/BusinessResult" + }, + "error": { + "$ref": "#/$defs/ExecutionError" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + } + }, + "required": [ + "id", + "requestId", + "event", + "status", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "status": { + "const": "SUCCEEDED" + } + } + }, + "then": { + "required": [ + "result" + ], + "not": { + "required": [ + "error" + ] + } + }, + "else": { + "not": { + "required": [ + "result" + ] + } + } + } + ] + }, + "AvailableEvent": { + "type": "object", + "properties": { + "event": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "inputSchema": { + "$ref": "flow-definition.schema.json#/$defs/InlineSchema" + }, + "ui": { + "$ref": "flow-definition.schema.json#/$defs/Ui" + } + }, + "required": [ + "event", + "inputSchema" + ], + "additionalProperties": false + }, + "BlockedEvent": { + "type": "object", + "properties": { + "event": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "reasonCode": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + } + }, + "required": [ + "event", + "reasonCode" + ], + "additionalProperties": false + }, + "AvailableEvents": { + "type": "object", + "properties": { + "requestId": { + "type": "string", + "format": "uuid" + }, + "revision": { + "type": "integer", + "minimum": 0 + }, + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/AvailableEvent" + }, + "minItems": 0, + "maxItems": 500 + }, + "blocked": { + "type": "array", + "items": { + "$ref": "#/$defs/BlockedEvent" + }, + "minItems": 0, + "maxItems": 500 + }, + "blockedByExecutionId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "requestId", + "revision", + "events", + "blocked" + ], + "additionalProperties": false, + "allOf": [ + { + "if": { + "required": [ + "blockedByExecutionId" + ] + }, + "then": { + "properties": { + "events": { + "maxItems": 0 + } + } + } + } + ] + }, + "Violation": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "pattern": "^(?:/(?:[^~/]|~[01])*)*$" + }, + "code": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "message": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + } + }, + "required": [ + "pointer", + "code", + "message" + ], + "additionalProperties": false + }, + "ValidationReport": { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/$defs/Violation" + }, + "minItems": 0, + "maxItems": 1000 + } + }, + "required": [ + "valid", + "errors" + ], + "additionalProperties": false, + "oneOf": [ + { + "properties": { + "valid": { + "const": true + }, + "errors": { + "maxItems": 0 + } + } + }, + { + "properties": { + "valid": { + "const": false + }, + "errors": { + "minItems": 1 + } + } + } + ] + }, + "Problem": { + "type": "object", + "properties": { + "type": { + "type": "string", + "format": "uri-reference" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "integer", + "minimum": 400, + "maximum": 599 + }, + "detail": { + "type": "string", + "maxLength": 2000 + }, + "instance": { + "type": "string", + "format": "uri-reference" + }, + "code": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "correlationId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/$defs/Violation" + }, + "minItems": 0, + "maxItems": 1000 + } + }, + "required": [ + "type", + "title", + "status", + "code", + "correlationId" + ], + "additionalProperties": false + }, + "AuditEntry": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "requestId": { + "type": "string", + "format": "uuid" + }, + "executionId": { + "type": "string", + "format": "uuid" + }, + "kind": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "actorSubject": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "flowId": { + "type": "string", + "format": "uuid" + }, + "fromState": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "toState": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "detail": { + "$ref": "#/$defs/Data" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + } + }, + "required": [ + "id", + "requestId", + "kind", + "actorSubject", + "flowId", + "detail", + "createdAt" + ], + "additionalProperties": false + }, + "HistoryPage": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/$defs/AuditEntry" + }, + "minItems": 0, + "maxItems": 100 + }, + "nextCursor": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + } + }, + "required": [ + "items" + ], + "additionalProperties": false + }, + "FlowView": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": [ + "DRAFT", + "PUBLISHED", + "RETIRED" + ] + }, + "revision": { + "type": "integer", + "minimum": 0 + }, + "definition": { + "$ref": "flow-definition.schema.json" + }, + "checksum": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + }, + "createdBy": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "publishedAt": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + } + }, + "required": [ + "id", + "status", + "revision", + "definition", + "createdAt", + "createdBy" + ], + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "status": { + "type": "string", + "enum": [ + "PUBLISHED", + "RETIRED" + ] + } + } + }, + "then": { + "required": [ + "checksum", + "publishedAt" + ] + } + } + ] + }, + "ActivateFlow": { + "type": "object", + "properties": { + "flowId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "flowId" + ], + "additionalProperties": false + }, + "RequestTypeView": { + "type": "object", + "properties": { + "typeKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "activeFlowId": { + "type": "string", + "format": "uuid" + }, + "enabled": { + "type": "boolean" + }, + "revision": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "typeKey", + "activeFlowId", + "enabled", + "revision" + ], + "additionalProperties": false + }, + "SimulationInput": { + "type": "object", + "properties": { + "state": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "requestData": { + "$ref": "#/$defs/Data" + }, + "event": { + "$ref": "#/$defs/EventCommand" + }, + "actor": { + "type": "object", + "properties": { + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "minItems": 0, + "uniqueItems": true, + "maxItems": 100 + }, + "businessScope": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + }, + "required": [ + "subject", + "roles", + "businessScope" + ], + "additionalProperties": false + }, + "results": { + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "additionalProperties": { + "type": "object" + } + } + }, + "required": [ + "state", + "requestData", + "event", + "actor", + "results" + ], + "additionalProperties": false + }, + "SimulationResult": { + "type": "object", + "properties": { + "transitionId": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "outcome": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "toState": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "data": { + "$ref": "#/$defs/Data" + }, + "matchedRuleIds": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "minItems": 0, + "maxItems": 1000 + } + }, + "required": [ + "transitionId", + "outcome", + "toState", + "data", + "matchedRuleIds" + ], + "additionalProperties": false + }, + "ReconciliationCommand": { + "oneOf": [ + { + "type": "object", + "properties": { + "mode": { + "const": "RESUME" + }, + "evidence": { + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "reason": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + } + }, + "required": [ + "reference", + "reason" + ], + "additionalProperties": false + }, + "verifiedActions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "actionId": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "result": { + "$ref": "#/$defs/Data" + }, + "providerReference": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + }, + "required": [ + "actionId", + "result" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 50 + } + }, + "required": [ + "mode", + "evidence", + "verifiedActions" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "mode": { + "const": "CLOSE_WITHOUT_TRANSITION" + }, + "evidence": { + "type": "object", + "properties": { + "reference": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "reason": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + } + }, + "required": [ + "reference", + "reason" + ], + "additionalProperties": false + }, + "effectStatus": { + "type": "string", + "enum": [ + "ABSENT_VERIFIED", + "COMPENSATED_VERIFIED" + ] + } + }, + "required": [ + "mode", + "evidence", + "effectStatus" + ], + "additionalProperties": false + } + ] + }, + "ReconciliationView": { + "type": "object", + "properties": { + "resolutionId": { + "type": "string", + "format": "uuid" + }, + "executionId": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": [ + "PENDING", + "FAILED" + ] + }, + "resolvedAt": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + } + }, + "required": [ + "resolutionId", + "executionId", + "status", + "resolvedAt" + ], + "additionalProperties": false + } + } +} diff --git a/schemas/flow-definition.schema.json b/schemas/flow-definition.schema.json new file mode 100644 index 0000000..4811eb7 --- /dev/null +++ b/schemas/flow-definition.schema.json @@ -0,0 +1,885 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Flow definition DSL v1", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "flowKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "version": { + "type": "integer", + "minimum": 1 + }, + "initialState": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "dataSchema": { + "$ref": "#/$defs/InlineSchema" + }, + "initialDataSchema": { + "$ref": "#/$defs/InlineSchema" + }, + "states": { + "type": "array", + "items": { + "$ref": "#/$defs/State" + }, + "minItems": 1, + "maxItems": 200 + }, + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/Event" + }, + "minItems": 0, + "maxItems": 500 + }, + "transitions": { + "type": "array", + "items": { + "$ref": "#/$defs/Transition" + }, + "minItems": 0, + "maxItems": 500 + }, + "decisions": { + "type": "array", + "items": { + "$ref": "#/$defs/Decision" + }, + "minItems": 0, + "maxItems": 500 + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/$defs/Task" + }, + "minItems": 0, + "maxItems": 500 + } + }, + "required": [ + "schemaVersion", + "flowKey", + "version", + "initialState", + "states", + "events", + "transitions", + "decisions", + "tasks" + ], + "additionalProperties": false, + "$defs": { + "Key": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "ContextPath": { + "type": "string", + "pattern": "^/(?:request/data|event/data|results|actor)(?:/(?:[^~/]|~[01])*)*$" + }, + "DataPointer": { + "type": "string", + "pattern": "^/(?:[^~/]|~[01])+(?:/(?:[^~/]|~[01])*)*$" + }, + "Value": { + "oneOf": [ + { + "type": "object", + "properties": { + "path": { + "type": "string", + "pattern": "^/(?:request/data|event/data|results|actor)(?:/(?:[^~/]|~[01])*)*$" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "literal": {} + }, + "required": [ + "literal" + ], + "additionalProperties": false + } + ] + }, + "InlineSchema": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "type": { + "oneOf": [ + { + "type": "string", + "enum": [ + "object", + "array", + "string", + "number", + "integer", + "boolean", + "null" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "object", + "array", + "string", + "number", + "integer", + "boolean", + "null" + ] + }, + "minItems": 1, + "uniqueItems": true, + "maxItems": 7 + } + ] + }, + "title": { + "type": "string", + "maxLength": 200 + }, + "description": { + "type": "string", + "maxLength": 2000 + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/InlineSchema" + } + }, + "required": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0, + "uniqueItems": true, + "maxItems": 200 + }, + "additionalProperties": { + "$ref": "#/$defs/InlineSchema" + }, + "items": { + "$ref": "#/$defs/InlineSchema" + }, + "enum": { + "type": "array", + "items": {}, + "minItems": 1, + "uniqueItems": true, + "maxItems": 1000 + }, + "const": {}, + "minLength": { + "type": "integer", + "minimum": 0 + }, + "maxLength": { + "type": "integer", + "minimum": 0 + }, + "pattern": { + "type": "string", + "maxLength": 1000 + }, + "format": { + "type": "string", + "enum": [ + "uuid", + "date-time", + "date", + "email", + "uri" + ] + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0 + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "uniqueItems": { + "type": "boolean" + }, + "minProperties": { + "type": "integer", + "minimum": 0 + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "allOf": { + "type": "array", + "items": { + "$ref": "#/$defs/InlineSchema" + }, + "minItems": 1, + "maxItems": 100 + }, + "anyOf": { + "type": "array", + "items": { + "$ref": "#/$defs/InlineSchema" + }, + "minItems": 1, + "maxItems": 100 + }, + "oneOf": { + "type": "array", + "items": { + "$ref": "#/$defs/InlineSchema" + }, + "minItems": 1, + "maxItems": 100 + }, + "not": { + "$ref": "#/$defs/InlineSchema" + }, + "if": { + "$ref": "#/$defs/InlineSchema" + }, + "then": { + "$ref": "#/$defs/InlineSchema" + }, + "else": { + "$ref": "#/$defs/InlineSchema" + } + }, + "required": [], + "additionalProperties": false + } + ] + }, + "Condition": { + "oneOf": [ + { + "type": "object", + "properties": { + "op": { + "type": "string", + "enum": [ + "EQ", + "NEQ", + "GT", + "GTE", + "LT", + "LTE", + "IN" + ] + }, + "left": { + "$ref": "#/$defs/Value" + }, + "right": { + "$ref": "#/$defs/Value" + } + }, + "required": [ + "op", + "left", + "right" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "op": { + "const": "EXISTS" + }, + "value": { + "type": "object", + "properties": { + "path": { + "type": "string", + "pattern": "^/(?:request/data|event/data|results|actor)(?:/(?:[^~/]|~[01])*)*$" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + } + }, + "required": [ + "op", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "enum": [ + "AND", + "OR" + ] + }, + "args": { + "type": "array", + "items": { + "$ref": "#/$defs/Condition" + }, + "minItems": 2, + "maxItems": 1000 + } + }, + "required": [ + "op", + "args" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "op": { + "const": "NOT" + }, + "arg": { + "$ref": "#/$defs/Condition" + } + }, + "required": [ + "op", + "arg" + ], + "additionalProperties": false + } + ] + }, + "RetryPolicy": { + "type": "object", + "properties": { + "maxAttempts": { + "type": "integer", + "minimum": 1, + "maximum": 3 + }, + "initialBackoffMs": { + "type": "integer", + "minimum": 100, + "maximum": 60000 + }, + "maxBackoffMs": { + "type": "integer", + "minimum": 100, + "maximum": 120000 + } + }, + "required": [ + "maxAttempts" + ], + "additionalProperties": false + }, + "Action": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "resultKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "retry": { + "$ref": "#/$defs/RetryPolicy" + }, + "type": { + "const": "INVOKE_INTEGRATION" + }, + "integrationKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "contractVersion": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "profile": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "profileVersion": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "inputs": { + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "additionalProperties": { + "$ref": "#/$defs/Value" + }, + "maxProperties": 200 + } + }, + "required": [ + "id", + "type", + "integrationKey", + "inputs", + "resultKey" + ], + "additionalProperties": false, + "dependentRequired": { + "profileVersion": [ + "profile" + ] + } + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "resultKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "retry": { + "$ref": "#/$defs/RetryPolicy" + }, + "type": { + "const": "REGISTER_DOCUMENT" + }, + "documentType": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "inputs": { + "type": "object", + "properties": { + "documentId": { + "$ref": "#/$defs/Value" + } + }, + "required": [ + "documentId" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "documentType", + "inputs", + "resultKey" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "type": { + "const": "NO_OP" + }, + "resultKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "output": { + "type": "object" + } + }, + "required": [ + "id", + "type", + "resultKey" + ], + "additionalProperties": false + } + ], + "description": "Tipos del MVP. NO_OP sólo simulación; publicación fija versiones de contrato/perfil desde catálogo confiable." + }, + "State": { + "type": "object", + "properties": { + "key": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "terminal": { + "type": "boolean" + } + }, + "required": [ + "key", + "terminal" + ], + "additionalProperties": false + }, + "Event": { + "type": "object", + "properties": { + "key": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "inputSchema": { + "$ref": "#/$defs/InlineSchema" + } + }, + "required": [ + "key", + "inputSchema" + ], + "additionalProperties": false + }, + "Update": { + "type": "object", + "properties": { + "target": { + "type": "string", + "pattern": "^/(?:[^~/]|~[01])+(?:/(?:[^~/]|~[01])*)*$" + }, + "value": { + "$ref": "#/$defs/Value" + } + }, + "required": [ + "target", + "value" + ], + "additionalProperties": false, + "description": "target es relativo a request.data, nunca a la solicitud completa." + }, + "Transition": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "from": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "event": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "allowedRoles": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "minItems": 1, + "uniqueItems": true, + "maxItems": 100 + }, + "guard": { + "$ref": "#/$defs/Condition" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/Action" + }, + "minItems": 0, + "maxItems": 50 + }, + "decision": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "destinations": { + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "additionalProperties": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "minProperties": 1 + }, + "to": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "outcome": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "updates": { + "type": "array", + "items": { + "$ref": "#/$defs/Update" + }, + "minItems": 0, + "maxItems": 200 + } + }, + "required": [ + "id", + "from", + "event", + "allowedRoles", + "actions" + ], + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "to", + "outcome" + ], + "not": { + "anyOf": [ + { + "required": [ + "decision" + ] + }, + { + "required": [ + "destinations" + ] + } + ] + } + }, + { + "required": [ + "decision", + "destinations" + ], + "not": { + "anyOf": [ + { + "required": [ + "to" + ] + }, + { + "required": [ + "outcome" + ] + } + ] + } + } + ] + }, + "Rule": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "when": { + "$ref": "#/$defs/Condition" + }, + "output": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + } + }, + "required": [ + "id", + "when", + "output" + ], + "additionalProperties": false + }, + "Decision": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "hitPolicy": { + "type": "string", + "enum": [ + "UNIQUE", + "FIRST" + ] + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/$defs/Rule" + }, + "minItems": 1, + "maxItems": 1000 + }, + "defaultOutput": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + } + }, + "required": [ + "id", + "hitPolicy", + "rules", + "defaultOutput" + ], + "additionalProperties": false + }, + "Ui": { + "oneOf": [ + { + "type": "object", + "properties": { + "component": { + "const": "DOCUMENT_UPLOAD" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "documentType": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "acceptedMimeTypes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9.+-]+/[A-Za-z0-9.+-]+$" + }, + "minItems": 1, + "uniqueItems": true, + "maxItems": 20 + }, + "maxBytes": { + "type": "integer", + "minimum": 1, + "maximum": 104857600 + } + }, + "required": [ + "component", + "title", + "documentType", + "acceptedMimeTypes", + "maxBytes" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "const": "FORM" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "fieldOrder": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0, + "uniqueItems": true, + "maxItems": 200 + } + }, + "required": [ + "component", + "title" + ], + "additionalProperties": false + } + ] + }, + "Task": { + "type": "object", + "properties": { + "state": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "event": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "ui": { + "$ref": "#/$defs/Ui" + }, + "inputSchema": { + "$ref": "#/$defs/InlineSchema" + } + }, + "required": [ + "state", + "event", + "ui", + "inputSchema" + ], + "additionalProperties": false + } + } +} diff --git a/schemas/integration-results.schema.json b/schemas/integration-results.schema.json new file mode 100644 index 0000000..0ec105a --- /dev/null +++ b/schemas/integration-results.schema.json @@ -0,0 +1,118 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Normalized example integration results v1", + "$defs": { + "CustomerResult": { + "type": "object", + "properties": { + "found": { + "type": "boolean" + }, + "customerId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + }, + "required": [ + "found" + ], + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "found": { + "const": true + } + } + }, + "then": { + "required": [ + "customerId" + ] + } + } + ] + }, + "BureauResult": { + "type": "object", + "properties": { + "score": { + "type": "number" + }, + "consultationId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + }, + "required": [ + "score", + "consultationId" + ], + "additionalProperties": false + }, + "DocumentValidationResult": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "VALID", + "INVALID" + ] + }, + "reasonCodes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "minItems": 0, + "maxItems": 100 + } + }, + "required": [ + "status", + "reasonCodes" + ], + "additionalProperties": false + }, + "RegisteredDocumentResult": { + "type": "object", + "properties": { + "documentId": { + "type": "string", + "format": "uuid" + }, + "documentType": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,99}$" + }, + "mimeType": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "sizeBytes": { + "type": "integer", + "minimum": 0 + }, + "checksum": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "documentId", + "documentType", + "mimeType", + "sizeBytes", + "checksum" + ], + "additionalProperties": false + } + } +} diff --git a/specs/DECISIONS.md b/specs/DECISIONS.md index 21b7b0f..205c4b8 100644 --- a/specs/DECISIONS.md +++ b/specs/DECISIONS.md @@ -2,7 +2,7 @@ ## ADR-001 — Base reproducible (2026-10-26) -Estado: seleccionada; verificación de runtime JDK 21 y PostgreSQL pendiente en T01. +Estado: verificada para T01 con compilación JDK 21 y smoke de salud contra server02; ver ADR-004 y evidencia E09 en PROGRESS. Pruebas de persistencia e integración completas corresponden a tareas posteriores. | Componente | Selección | Motivo | |---|---|---| @@ -41,3 +41,32 @@ Por instrucción expresa del usuario se adelanta el empaquetado del arquetipo: C Dockerfile multietapa con Temurin 21.0.12.1_1 (JDK para construir, JRE para ejecutar), tags verificados en el [catálogo oficial](https://github.com/docker-library/official-images/blob/master/library/eclipse-temurin). Runtime no root y healthcheck de readiness. Maven Wrapper construye con package y ejecuta pruebas unitarias; verify con integración permanece en CI. El contexto permite sólo fuentes, POM y Wrapper; excluye .env, target y archivos del IDE. Compose exige DB_URL/usuario/contraseña y parámetros JWT, conserva el puerto interno 8080 y publica localhost:8080 por defecto. Se incluye host-gateway para PostgreSQL en el anfitrión. La construcción y conexión real están pendientes de un entorno Docker y valores externos. Referencias: [build multietapa](https://docs.docker.com/build/building/multi-stage/) e [interpolación de Compose](https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/). + +## ADR-004 — Cierre verificable de T01 con despliegue en la red local (2026-10-26) + +El servicio ya está desplegado por el usuario en server02:8787 con PostgreSQL externo. Se interpreta el health local de T01 como comprobación del entorno de desarrollo en esa red, sin requerir duplicar el despliegue en el equipo del agente. La compilación se realiza localmente con Temurin JDK 21 oficial, descargado a .tools y validado por SHA256 del proveedor; no se modifica el Java del sistema. + +El perfil explícito deployment-smoke ejecuta cinco pruebas Failsafe de sólo lectura contra una URL obligatoria: health, readiness, liveness, rechazo sin token y rechazo con token malformado. No se omiten errores de conectividad ni pruebas fallidas. Se preservan los patrones normales de integración *IT, IT* y *ITCase al activar el perfil. + +El build de T01 y la salud del despliegue se verifican por separado: no se afirma que el JAR recién generado haya sido desplegado ni se acredita el runtime remoto por una respuesta health. No se redepliega el servidor ni se accede directamente a sus credenciales o base. Testcontainers, migraciones, JWT válido/roles y A01–A20 siguen pendientes; este smoke no los sustituye. CI no depende de server02: el perfil requiere activación explícita fuera del build normal. + +## ADR-005 — Contratos T02: REST y DSL v1 (2026-10-26) + +Estado: implementado como contrato y pruebas, no como endpoints. OpenAPI 3.1.0 y JSON Schema 2020-12 en archivos separados con referencias relativas locales. No se incorpora springdoc: la documentación contractual es independiente de Boot y no habilita rutas administrativas antes de T07. + +Se concreta lo no detallado por la especificación sin cambiar idempotencia, aislamiento de versiones ni autorización: + +- Rutas fijas usan to/outcome; rutas calculadas usan decision/destinations, con el output de decisión como resultado de negocio. EXISTS usa value.path; NOT usa arg; AND/OR usan args. Mappings admiten exactamente path o literal. +- dataSchema e initialDataSchema permiten describir datos finales/entrada inicial. Borradores y ejemplo mínimo original pueden omitirlos estructuralmente; publicación T06 exige dataSchema de objeto. InitialDataSchema ausente utiliza dataSchema. No se confunde validez estructural con publicación válida. +- Schemas embebidos limitan el vocabulario a validación declarativa y no admiten referencias externas. Se conservan boolean schemas y combinadores. Se rechazan propiedades desconocidas de DSL/DTOs; datos configurables requieren segunda validación contra su schema de evento/flujo. Rechazo de JSON con claves duplicadas precede validación. +- JSON Pointer de updates es relativo a request.data. Restricciones de identidad/ACL permanecen fuera de ese espacio. Publicación verifica destinos solapados y schemas resultantes; runtime verifica tipos/ambigüedad. Estas comprobaciones no se simulan en T02. +- Publicar y retirar exigen If-Match adicional al exigido por eventos/edición/activación. Esto concreta su concurrencia y evita operar sobre una revisión diferente a la revisada. Alta de request_type se provisionará por migración/configuración en T03/T06; activar uno inexistente devuelve 404. +- Conciliación acepta RESUME con evidencia y resultados verificados por acción, o CLOSE_WITHOUT_TRANSITION con ausencia/compensación comprobada. Prohíbe escoger destino. RESUME reencola PENDING; cierre marca FAILED sin cambiar negocio. Idempotencia durable, autorización por scope y evidencia verificada son requisitos para T12. +- Replays de creación devuelven 201 con la misma identidad; eventos devuelven 202 con executionId estable y status actual. Los errores tienen code/correlationId; fechas UTC Z, opcionales omitidos, cursor opaco máximo 100. +- Se fijan techos estructurales auxiliares: 500 eventos/decisiones/tareas, 100 roles por transición, 200 updates, reintentos máximo 3 y tamaño documental máximo 100 MiB en DSL. Los límites operativos podrán ser menores; los 1 MiB/256 KiB y límites AST de la especificación requieren control adicional antes/durante deserialización en T06/T09. + +Herramientas fijadas, únicamente scope test: networknt json-schema-validator 1.5.9 (2020-12 con format assertions) y swagger-parser-v3 2.1.35 (OpenAPI 3.1). Jackson 2 de estas herramientas convive sólo en tests con Jackson 3 del runtime Boot 4.1.2. Se comprobó compatibilidad mediante clean verify con Java 21. + +Se conserva una copia del schema oficial OpenAPI 3.1 (2022-10-07), SHA256 DA01BA28852CAC0DE53893797CB8D1942BC3B05084F526DCC216717DEC314ED0, para validar sin red. Swagger Parser con resolve=true intentó expandir indefinidamente las referencias recursivas del AST: se usa resolve=false y comprobación explícita de todos los $ref locales. Networknt valida los schemas y ejemplos reales. No se relajan límites ni se ignoran referencias rotas para resolver esa incompatibilidad. + +Referencias: [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0), [schema oficial](https://spec.openapis.org/oas/3.1/schema/2022-10-07), [networknt](https://github.com/networknt/json-schema-validator), [Swagger Parser y soporte 3.1](https://github.com/swagger-api/swagger-parser). Detalles de uso en contracts/README.md y schemas/README.md. diff --git a/specs/IMPLEMENTATION_PLAN.md b/specs/IMPLEMENTATION_PLAN.md index 9e70d94..aa821b6 100644 --- a/specs/IMPLEMENTATION_PLAN.md +++ b/specs/IMPLEMENTATION_PLAN.md @@ -30,23 +30,27 @@ El alcance actual es crear el arquetipo y dejar preparada la ejecución del plan Dependencias: —. Entregables: pom.xml, mvnw*, .mvn, configuración, ADR-001. -- [ ] Fijar versiones y BOM. -- [ ] Generar Wrapper oficial. -- [ ] Preparar paquetes y configuración. -- [ ] Ejecutar compilación y smoke test de salud con JDK 21/PostgreSQL. +- [x] Fijar versiones y BOM. +- [x] Generar Wrapper oficial. +- [x] Preparar paquetes y configuración. +- [x] Ejecutar compilación y smoke test de salud con JDK 21/PostgreSQL. Verificación: JDK 21; ./mvnw clean verify; arranque local y HTTP health. Cierre: Wrapper reproducible, compilación y health local comprobados. +Cerrado el 2026-10-26: `clean verify -Pdeployment-smoke -Dsmoke.baseUrl=http://server02:8787` con JDK 21.0.12.1 y Maven 3.9.12, 5 pruebas aprobadas, 0 omitidas. Compilación en el equipo de desarrollo y smoke sobre el despliegue de la red local con PostgreSQL externo, conforme a ADR-004. Evidencia detallada en PROGRESS. + ## T02 — Contratos OpenAPI y JSON Schema del DSL Dependencias: T01. Entregables: contracts/openapi.yaml, schemas/, ejemplos contractuales. -- [ ] Definir rutas, DTOs y errores. -- [ ] Definir schema del flujo/AST/tareas. -- [ ] Validar ejemplos y casos inválidos automáticamente. +- [x] Definir rutas, DTOs y errores. +- [x] Definir schema del flujo/AST/tareas. +- [x] Validar ejemplos y casos inválidos automáticamente. Verificación: Validador OpenAPI/JSON Schema dentro de verify. Cierre: OpenAPI 3.1 y schemas completos; ejemplos validados. +Cerrado el 2026-10-26: clean verify con Java 21, 92 comprobaciones, 0 fallos/errores/omisiones. Contrato de 14 operaciones, DSL 2020-12, DTOs y resultados normalizados de ejemplo. Las pruebas verifican estructura/ejemplos/referencias, no comportamiento del motor; ver ADR-005 y evidencia E10. + ## T03 — Modelo PostgreSQL y Flyway Dependencias: T02. Entregables: db/migration/, pruebas *IT. diff --git a/specs/PROGRESS.md b/specs/PROGRESS.md index 7465d8c..9eb0d60 100644 --- a/specs/PROGRESS.md +++ b/specs/PROGRESS.md @@ -1,14 +1,14 @@ # Seguimiento de implementación -Actualizado: 2026-10-26. Entrega actual: arquetipo y planificación. El motor no está implementado. +Actualizado: 2026-10-26. T01 y T02 DONE. Siguiente tarea: T03 (PostgreSQL/Flyway). El motor y endpoints de negocio no están implementados. Estados permitidos: PENDING, IN_PROGRESS, BLOCKED, DONE. No más de una tarea principal IN_PROGRESS. Consultar IMPLEMENTATION_PLAN para subtareas y criterios. | ID | Estado | Dependencias | Archivos | Comandos/checks | Resultado | Bloqueo | Siguiente paso | |---|---|---|---|---|---|---|---| -| T01 | BLOCKED | — | pom.xml, mvnw*, .mvn, configuración, ADR-001 | Ver evidencia E01–E04 | Arquetipo creado; package con JDK 24 y release 21 aprobado; cierre pendiente | JDK 21 y Docker/PostgreSQL no disponibles para validación local completa | Validar con JDK 21 y PostgreSQL; añadir smoke test de salud | -| T02 | PENDING | T01 | contracts/openapi.yaml, schemas/, ejemplos contractuales | Pendiente: Validador OpenAPI/JSON Schema dentro de verify | No ejecutado | Dependencias sin completar | Definir rutas, DTOs y errores | -| T03 | PENDING | T02 | db/migration/, pruebas *IT | Pendiente: Testcontainers PostgreSQL, rollback y migraciones | No ejecutado | Dependencias sin completar | Crear tablas, FKs e índices | +| 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 | Pendiente: Testcontainers PostgreSQL, rollback y migraciones | No ejecutado | Docker requerido para pruebas; dependencia T02 satisfecha | Crear tablas, FKs e índices y preparar entorno Testcontainers | | 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 | @@ -24,7 +24,34 @@ Estados permitidos: PENDING, IN_PROGRESS, BLOCKED, DONE. No más de una tarea pr | 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 | | 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 | -## Evidencias — 2026-10-26 +## E10 — Cierre T02 — 2026-10-26 11:58 (America/Mexico_City) + +- Estado: DONE. 14 operaciones REST OpenAPI 3.1.0, DTOs, schema DSL 2020-12, ejemplos de resultados normalizados y fixtures contractuales completos para esta fase. +- Comando: `.\mvnw.cmd --batch-mode --no-transfer-progress clean verify` con JAVA_HOME=.tools/jdk21/jdk-21.0.12.1+1, Maven Wrapper 3.9.12 y Enforcer habilitado. +- Resultado: BUILD SUCCESS (11.969 s); ContractValidationTest: 92 comprobaciones, 0 fallos, 0 errores, 0 omitidas. Reportes en target/surefire-reports/com.organizacion.orchestrator.ContractValidationTest.txt y TEST-com.organizacion.orchestrator.ContractValidationTest.xml. +- Cobertura: schema oficial OpenAPI 3.1, parseo Swagger, meta-schema 2020-12, resolución de todos los $ref locales, 52 fixtures (29 válidos/23 inválidos), ejemplos embebidos, rutas/seguridad/cabeceras, aridad de condiciones, límites estructurales, UUID, claves JSON duplicadas y rechazo de inyección de datos de proveedor según inputSchema. +- Corrección durante la tarea: primera ejecución tuvo 91/92 aprobadas; Swagger Parser expandía el schema recursivo hasta exceder profundidad. Se corrigió con resolve=false, manteniendo verificación independiente de referencias y validación real de ejemplos. Segunda ejecución aprobó todo; no hubo tests omitidos. +- Dependencias sólo test: networknt 1.5.9 y swagger-parser-v3 2.1.35; API Jackson 2 gestionada por BOM. Inspección del JAR con zipfile confirmó ausencia de validadores/Swagger y Jackson 2 databind/YAML en BOOT-INF/lib. +- Dockerfile/.dockerignore incluyen contracts, schemas y examples/contracts en build para ejecutar estas pruebas; no se construyó una imagen en esta sesión (Docker no disponible en este equipo). No se modificó ni redesplegó server02. +- Archivos principales: contracts/openapi.yaml, contracts/validation/openapi-3.1.schema.json, schemas/{flow-definition,api,integration-results}.schema.json, examples/contracts/manifest.json y fixtures, examples/http/contracts.http, pom.xml, ContractValidationTest.java, README/documentación y ADR-005. +- Alcance: validación de contratos y datos, sin endpoints, base, providers ni motor. La evaluación de scoring/AST, validación semántica de grafo, permisos efectivos y A01–A20 corresponden a tareas posteriores; no se declaran aprobados. El smoke remoto de T01 no se repitió por no haber cambios de runtime. +- Siguiente tarea: T03 (PENDING), dependencias satisfechas; requiere entorno Docker/PostgreSQL para Testcontainers. + +## E09 — Cierre T01 — 2026-10-26 11:20 (America/Mexico_City) + +- Estado final: DONE. Esta evidencia supera los bloqueos históricos de T01 descritos abajo; no cierra T07/T16/T17. +- JDK: Eclipse Temurin 21.0.12.1+1, instalado sólo en .tools/jdk21/jdk-21.0.12.1+1 (ignorado en Git). Descarga oficial mediante API Adoptium, SHA256 comprobado: f9d6e191ab098c0d416e7d588a24420a8621cd2f4720dab2459b8b7b2d2d8b4e. +- Maven Wrapper: 3.9.12. `mvnw.cmd -version` confirmó JDK 21.0.12.1. Enforcer validó Java y Maven; no se omitió ninguna regla. +- Comando: `.\mvnw.cmd --batch-mode --no-transfer-progress clean verify -Pdeployment-smoke '-Dsmoke.baseUrl=http://server02:8787'`, con JAVA_HOME apuntando al JDK anterior sólo en esa sesión. +- Resultado: BUILD SUCCESS (29.285 s); 5 pruebas ejecutadas, 0 fallos, 0 errores, 0 omitidas. Compilación release 21 y JAR ejecutable generado. +- Pruebas: health/readiness/liveness HTTP 200 y JSON status UP; ruta de solicitudes sin token HTTP 401 y challenge Bearer; token malformado HTTP 401. +- Reportes: target/failsafe-reports/com.organizacion.orchestrator.DeploymentSmokeCheck.txt y TEST-com.organizacion.orchestrator.DeploymentSmokeCheck.xml. Son artefactos locales; este registro conserva el resumen sin copiar propiedades del entorno. +- JAR: target/orquestador-nmp-0.1.0-SNAPSHOT.jar, SHA256 BA94B68388E26575F3CB6370E360B7E1D2C859853359CEE1D2D4C931D884B030. +- Archivos: pom.xml, src/test/java/com/organizacion/orchestrator/DeploymentSmokeCheck.java, README de pruebas, README.md, specs/RUNBOOK.md, specs/IMPLEMENTATION_PLAN.md, specs/DECISIONS.md y este registro. +- Alcance: build local con Java 21 más smoke del despliegue en red local con PostgreSQL externo, documentado en ADR-004. No hubo redeploy ni prueba de JWT válido, roles, endpoints funcionales o persistencia de negocio. La salud no certifica versión del runtime remoto ni identidad de su JAR. +- Siguiente tarea habilitada: T02 (PENDING, sin bloqueo de dependencias). + +## Evidencias históricas — 2026-10-26 - E01: `java -version` y `mvn -version`: Java 17.0.12 y Maven 3.9.12. Se encontró también Corretto 24; no JDK 21 en las ubicaciones revisadas. `Get-Command docker` no encontró Docker en PATH. - E02: `mvn -B -N org.apache.maven.plugins:maven-wrapper-plugin:3.3.4:wrapper '-Dmaven=3.9.12' '-Dtype=only-script'`: BUILD SUCCESS. Wrapper oficial creado con distribución 3.9.12. Se corrigió una primera invocación cuya propiedad sin comillas fue fragmentada por PowerShell. @@ -36,6 +63,19 @@ Estados permitidos: PENDING, IN_PROGRESS, BLOCKED, DONE. No más de una tarea pr ## Próxima sesión +### E08 — Prueba del servicio remoto (2026-10-26) + +Comprobación ejecutada con Invoke-WebRequest -UseBasicParsing -TimeoutSec 10 contra http://server02:8787: + +| GET | HTTP | Resultado | +|---|---|---| +| /actuator/health | 200 | status UP | +| /actuator/health/readiness | 200 | status UP | +| /actuator/health/liveness | 200 | status UP | +| /api/v1/requests, sin token | 401 | Protección sin autenticación confirmada | + +Con la configuración del repositorio, readiness incluye PostgreSQL. Esta prueba confirma salud remota y rechazo sin token; no acredita la existencia del endpoint de solicitudes, validación de JWT real, versión del runtime remoto ni la suite clean verify. El intento anterior contra serer02 falló por nombre incorrecto y se corrigió a server02. Las limitaciones de arranque remoto registradas anteriormente quedan superadas por esta evidencia; T01 conserva pendiente el cierre reproducible con Java 21. + ### Preparación Docker solicitada — 2026-10-26 - Archivos: Dockerfile, .dockerignore, compose.yaml, .env.example, README.md, RUNBOOK.md y ADR-003. @@ -45,8 +85,8 @@ Estados permitidos: PENDING, IN_PROGRESS, BLOCKED, DONE. No más de una tarea pr - 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. Resolver T01: configurar JDK 21, PostgreSQL y valores de entorno JWT; incorporar y ejecutar smoke test, `clean verify` y health local. -3. Registrar evidencia y marcar T01 DONE sólo cuando cumpla todos sus criterios; entonces iniciar T02. +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. ## Plantilla de evidencia adicional diff --git a/specs/RUNBOOK.md b/specs/RUNBOOK.md index 28677a7..0e222a9 100644 --- a/specs/RUNBOOK.md +++ b/specs/RUNBOOK.md @@ -63,6 +63,14 @@ Configurar JDK 21 y las seis variables DB/JWT en la sesión o Run Configuration. ## Diagnóstico +Para automatizar la comprobación del despliegue con JDK 21: + +```powershell +.\mvnw.cmd clean verify -Pdeployment-smoke '-Dsmoke.baseUrl=http://server02:8787' +``` + +Son cinco pruebas de sólo lectura; resultados en `target/failsafe-reports`. El servicio debe estar iniciado con su PostgreSQL externo. La URL es obligatoria; no hay omisión silenciosa por falta de conectividad. El perfil no despliega la aplicación ni cambia la configuración del servidor. El build normal no ejecuta este smoke remoto, para no depender de la red privada desde CI. + - Variable faltante: completar el valor requerido en `.env`. - Conexión rechazada/timeout: revisar DNS, puerto, firewall y acceso desde Docker; no usar localhost para el anfitrión. - Error de autenticación PostgreSQL: comprobar usuario, contraseña y permisos. diff --git a/src/test/java/com/organizacion/orchestrator/ContractValidationTest.java b/src/test/java/com/organizacion/orchestrator/ContractValidationTest.java new file mode 100644 index 0000000..ae9d701 --- /dev/null +++ b/src/test/java/com/organizacion/orchestrator/ContractValidationTest.java @@ -0,0 +1,237 @@ +package com.organizacion.orchestrator; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SchemaValidatorsConfig; +import com.networknt.schema.SpecVersion; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.ParseOptions; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +/** Contract checks only: no HTTP, database, provider or running service is needed. */ +class ContractValidationTest { + private static final Path ROOT = Path.of("").toAbsolutePath().normalize(); + private static final Path OPENAPI = ROOT.resolve("contracts/openapi.yaml"); + private static final ObjectMapper JSON = new ObjectMapper().enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + private static final ObjectMapper YAML = new ObjectMapper(new YAMLFactory()).enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + private static final JsonSchemaFactory FACTORY = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); + private static final SchemaValidatorsConfig CONFIG = SchemaValidatorsConfig.builder().formatAssertionsEnabled(true).build(); + + private static JsonNode read(Path path) throws IOException { + return (path.toString().endsWith(".yaml") ? YAML : JSON).readTree(path.toFile()); + } + + private static JsonSchema schema(URI uri) { + return FACTORY.getSchema(SchemaLocation.of(uri.toString()), CONFIG); + } + + private static URI schemaUri(String reference) { + return ROOT.resolve("schemas/").toUri().resolve(reference); + } + + private static void valid(JsonSchema schema, JsonNode value) { + var errors = schema.validate(value); + assertTrue(errors.isEmpty(), () -> errors.toString()); + } + + @Test + void openApiConformsToOfficial31SchemaAndParser() throws Exception { + var document = read(OPENAPI); + valid(schema(ROOT.resolve("contracts/validation/openapi-3.1.schema.json").toUri()), document); + var options = new ParseOptions(); + // Recursive DSL references must remain references. Resolution is checked separately, + // and networknt validates instances against the actual external schema documents. + options.setResolve(false); + var parsed = new OpenAPIV3Parser().readLocation(OPENAPI.toString(), null, options); + assertNotNull(parsed.getOpenAPI(), () -> parsed.getMessages().toString()); + assertTrue(parsed.getMessages().isEmpty(), () -> parsed.getMessages().toString()); + assertEquals("3.1.0", parsed.getOpenAPI().getOpenapi()); + } + + @TestFactory + Stream schemaDocumentsConformTo202012MetaSchema() throws Exception { + var meta = schema(URI.create("https://json-schema.org/draft/2020-12/schema")); + try (var files = Files.list(ROOT.resolve("schemas"))) { + return files.filter(p -> p.toString().endsWith(".json")).sorted().toList().stream() + .map(p -> DynamicTest.dynamicTest(p.getFileName().toString(), () -> valid(meta, read(p)))); + } + } + + @TestFactory + Stream allFixtureExpectationsHold() throws Exception { + List tests = new ArrayList<>(); + var manifest = read(ROOT.resolve("examples/contracts/manifest.json")); + Set registered = new HashSet<>(); + for (var entry : manifest) { + String file = entry.path("file").asText(); + assertTrue(registered.add(file), "Duplicate fixture: " + file); + tests.add(DynamicTest.dynamicTest(file, () -> { + var instance = read(ROOT.resolve("examples/contracts").resolve(file)); + var errors = schema(schemaUri(entry.path("schema").asText())).validate(instance); + assertEquals(entry.path("valid").asBoolean(), errors.isEmpty(), () -> file + ": " + errors); + })); + } + try (var files = Files.walk(ROOT.resolve("examples/contracts"))) { + Set actual = new HashSet<>(); + files.filter(p -> p.toString().endsWith(".json") && !p.getFileName().toString().equals("manifest.json")) + .forEach(p -> actual.add(ROOT.resolve("examples/contracts").relativize(p).toString().replace('\\', '/'))); + assertEquals(actual, registered, "Every fixture must be registered and validated"); + } + return tests.stream(); + } + + @TestFactory + Stream everyOpenApiExampleMatchesItsSchema() throws Exception { + List tests = new ArrayList<>(); + walk(read(OPENAPI), node -> { + if (node.isObject() && node.has("schema") && node.has("example")) { + String reference = node.path("schema").path("$ref").asText(); + assertFalse(reference.isBlank(), "Media example requires a reusable schema"); + tests.add(DynamicTest.dynamicTest("OpenAPI example " + reference + " #" + tests.size(), + () -> valid(schema(OPENAPI.toUri().resolve(reference)), node.get("example")))); + } + }); + assertTrue(tests.size() >= 30, "Expected examples for every body and success/error response"); + return tests.stream(); + } + + @Test + void allReferencesAreLocalAndResolve() throws Exception { + List documents = new ArrayList<>(List.of(OPENAPI)); + try (var files = Files.list(ROOT.resolve("schemas"))) { + documents.addAll(files.filter(p -> p.toString().endsWith(".json")).toList()); + } + for (Path document : documents) { + walk(read(document), node -> { + if (node.has("$ref")) { + URI resolved = document.toUri().resolve(node.get("$ref").asText()); + assertEquals("file", resolved.getScheme(), "Contract references must not fetch from the network"); + try { + Path target = Path.of(new URI(resolved.getScheme(), resolved.getAuthority(), resolved.getPath(), null, null)).normalize(); + assertTrue(target.startsWith(ROOT), "Reference escapes repository"); + var targetDocument = read(target); + if (resolved.getFragment() != null) { + assertFalse(targetDocument.at(resolved.getFragment()).isMissingNode(), resolved.toString()); + } + } catch (Exception e) { + throw new AssertionError("Unresolved contract reference " + resolved, e); + } + } + }); + } + } + + @Test + void requiredRoutesSecurityAndConcurrencyHeadersArePresent() throws Exception { + JsonNode document = read(OPENAPI); + assertEquals("/api/v1", document.at("/servers/0/url").asText()); + assertTrue(document.at("/security/0").has("bearerAuth")); + Set expected = Set.of("POST /requests", "GET /requests/{id}", "GET /requests/{id}/available-events", + "POST /requests/{id}/events", "GET /executions/{id}", "GET /requests/{id}/history", + "POST /admin/flows", "PUT /admin/flows/{id}", "POST /admin/flows/{id}/validate", + "POST /admin/flows/{id}/simulate", "POST /admin/flows/{id}/publish", + "PUT /admin/request-types/{key}/active-flow", "POST /admin/flows/{id}/retire", + "POST /admin/executions/{id}/resolve"); + Set actual = new HashSet<>(); + Set operationIds = new HashSet<>(); + document.get("paths").fields().forEachRemaining(path -> path.getValue().fields().forEachRemaining(method -> { + var op = method.getValue(); + actual.add(method.getKey().toUpperCase() + " " + path.getKey()); + assertTrue(operationIds.add(op.path("operationId").asText())); + assertFalse(op.has("security"), "Do not override global JWT protection"); + assertTrue(op.path("responses").has("401")); + if (path.getKey().startsWith("/admin/")) assertFalse(op.path("x-required-roles").isEmpty()); + if (method.getKey().equals("post") && (path.getKey().equals("/requests") + || path.getKey().endsWith("/events") || path.getKey().endsWith("/resolve"))) { + assertParameter(op, "IdempotencyKey"); + } + if (method.getKey().equals("put") || path.getKey().endsWith("/events") + || path.getKey().endsWith("/publish") || path.getKey().endsWith("/retire")) { + assertParameter(op, "IfMatch"); + assertTrue(op.path("responses").has("412")); + assertTrue(op.path("responses").has("428")); + } + })); + assertEquals(expected, actual); + assertEquals(100, document.at("/components/parameters/Limit/schema/maximum").asInt()); + assertTrue(document.at("/paths/~1requests/post/responses/201/headers").has("Location")); + assertTrue(document.at("/paths/~1requests/post/responses/201/headers").has("ETag")); + assertTrue(document.at("/paths/~1requests~1{id}~1events/post/responses/202/headers").has("Location")); + } + + private static void assertParameter(JsonNode operation, String name) { + for (var parameter : operation.path("parameters")) { + if (parameter.path("$ref").asText().equals("#/components/parameters/" + name)) return; + } + fail("Missing parameter: " + name); + } + + @Test + void embeddedInputSchemasValidatePayloadsAndPreventProviderDataInjection() throws Exception { + for (String flow : List.of("scoring", "document", "form")) { + JsonNode definition = read(ROOT.resolve("examples/contracts/flows/" + flow + ".json")); + walk(definition, n -> { + for (String field : List.of("inputSchema", "dataSchema", "initialDataSchema")) { + if (n.has(field)) valid(schema(URI.create("https://json-schema.org/draft/2020-12/schema")), n.get(field)); + } + }); + } + var document = read(ROOT.resolve("examples/contracts/flows/document.json")); + var input = FACTORY.getSchema(document.at("/events/0/inputSchema"), CONFIG); + valid(input, JSON.readTree("{\"documentId\":\"00000000-0000-0000-0000-000000000123\"}")); + assertFalse(input.validate(JSON.readTree("{\"documentId\":\"bad-uuid\"}")).isEmpty()); + assertFalse(input.validate(JSON.readTree("{\"documentId\":\"00000000-0000-0000-0000-000000000123\",\"score\":99}")).isEmpty()); + assertFalse(input.validate(JSON.readTree("{\"url\":\"https://untrusted.example/document\"}")).isEmpty()); + } + + @Test + void structuralLimitsAndConditionArityAreEnforced() throws Exception { + var flow = (ObjectNode) read(ROOT.resolve("examples/contracts/flows/scoring.json")); + var schema = schema(schemaUri("flow-definition.schema.json")); + var tooManyStates = flow.deepCopy(); + var states = tooManyStates.withArray("states"); + while (states.size() < 201) states.addObject().put("key", "STATE_" + states.size()).put("terminal", true); + assertFalse(schema.validate(tooManyStates).isEmpty()); + var tooManyActions = flow.deepCopy(); + var actions = (com.fasterxml.jackson.databind.node.ArrayNode) tooManyActions.at("/transitions/0/actions"); + while (actions.size() < 51) actions.add(actions.get(0).deepCopy()); + assertFalse(schema.validate(tooManyActions).isEmpty()); + var condition = schema(schemaUri("flow-definition.schema.json#/$defs/Condition")); + valid(condition, JSON.readTree("{\"op\":\"NOT\",\"arg\":{\"op\":\"EXISTS\",\"value\":{\"path\":\"/event/data/x\"}}}")); + for (String invalid : List.of("{\"op\":\"NOT\",\"args\":[]}", "{\"op\":\"AND\",\"args\":[]}", + "{\"op\":\"GT\",\"left\":{\"literal\":1}}", "{\"op\":\"EXISTS\",\"value\":{\"literal\":true}}")) { + assertFalse(condition.validate(JSON.readTree(invalid)).isEmpty(), invalid); + } + } + + @Test + void duplicateJsonKeysAreRejectedBeforeValidation() { + assertThrows(IOException.class, () -> JSON.readTree("{\"schemaVersion\":1,\"schemaVersion\":2}")); + } + + private static void walk(JsonNode node, Consumer visitor) { + visitor.accept(node); + if (node.isContainerNode()) node.forEach(child -> walk(child, visitor)); + } +} diff --git a/src/test/java/com/organizacion/orchestrator/DeploymentSmokeCheck.java b/src/test/java/com/organizacion/orchestrator/DeploymentSmokeCheck.java new file mode 100644 index 0000000..d6e4c25 --- /dev/null +++ b/src/test/java/com/organizacion/orchestrator/DeploymentSmokeCheck.java @@ -0,0 +1,66 @@ +package com.organizacion.orchestrator; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Read-only checks against an explicitly selected deployment, without credentials. */ +class DeploymentSmokeCheck { + private static final HttpClient CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .followRedirects(HttpClient.Redirect.NEVER) + .build(); + private static URI baseUri; + + @BeforeAll + static void requireExplicitTarget() { + String configured = System.getProperty("smoke.baseUrl", ""); + assertTrue(configured.startsWith("http://") || configured.startsWith("https://"), + "Provide -Dsmoke.baseUrl=http://host:port with -Pdeployment-smoke"); + baseUri = URI.create(configured); + assertTrue(baseUri.getHost() != null && baseUri.getUserInfo() == null + && baseUri.getQuery() == null && baseUri.getFragment() == null + && (baseUri.getPath().isEmpty() || baseUri.getPath().equals("/")), + "smoke.baseUrl must be an origin without credentials, query, fragment or context path"); + } + + @ParameterizedTest + @ValueSource(strings = {"/actuator/health", "/actuator/health/readiness", "/actuator/health/liveness"}) + void healthIsUp(String path) throws Exception { + var response = CLIENT.send(request(path).build(), HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode(), path); + var json = JsonMapper.builder().build().readTree(response.body()); + assertEquals("\"UP\"", json.path("status").toString(), path); + } + + @Test + void requestWithoutTokenIsRejected() throws Exception { + var response = CLIENT.send(request("/api/v1/requests").build(), HttpResponse.BodyHandlers.discarding()); + assertEquals(401, response.statusCode()); + assertTrue(response.headers().firstValue("WWW-Authenticate").orElse("").startsWith("Bearer")); + } + + @Test + void malformedBearerTokenIsRejected() throws Exception { + var response = CLIENT.send(request("/api/v1/requests") + .header("Authorization", "Bearer not-a-jwt").build(), HttpResponse.BodyHandlers.discarding()); + assertEquals(401, response.statusCode()); + } + + private static HttpRequest.Builder request(String path) { + return HttpRequest.newBuilder(baseUri.resolve(path)) + .timeout(Duration.ofSeconds(15)) + .header("Accept", "application/json").GET(); + } +} diff --git a/src/test/java/com/organizacion/orchestrator/README.md b/src/test/java/com/organizacion/orchestrator/README.md index 1f7f690..42078e0 100644 --- a/src/test/java/com/organizacion/orchestrator/README.md +++ b/src/test/java/com/organizacion/orchestrator/README.md @@ -1,6 +1,11 @@ # Pruebas JUnit 5: clases *Test con Surefire durante test; clases *IT con Failsafe durante verify. -T01 incorpora el smoke test de arranque/health al disponer de Java 21 y PostgreSQL. +T02 incorpora ContractValidationTest: OpenAPI oficial, schemas 2020-12, referencias locales, ejemplos válidos/inválidos, cabeceras/seguridad contractual y límites de estructura. Se ejecuta en cada test/package/verify sin servicios externos. No equivale a probar el motor, autorización real o publicación semántica. +T01 incorpora DeploymentSmokeCheck: tres endpoints de salud UP, rechazo sin token y rechazo de token malformado. +Se ejecuta explícitamente con `./mvnw clean verify -Pdeployment-smoke -Dsmoke.baseUrl=http://host:port`. +El perfil exige una URL y falla ante servicio inaccesible, HTTP inesperado o salud distinta de UP; no omite pruebas. +Sólo efectúa GET sin credenciales y no modifica datos. No demuestra JWT válido ni la implementación de solicitudes. +El build normal/CI no contacta servidores privados: este smoke requiere un despliegue ya iniciado. T03 incorpora Testcontainers PostgreSQL; nunca sustituir concurrencia o JSONB por H2. No omitir integraciones silenciosamente cuando Docker no esté disponible.