Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DECISIONS.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ _Nota: Las decisiones universales se heredan del Upstream Base ([evolith_arch32]
| T-052 | Un solo MCP del ecosistema (el del Core); el gateway consume, no compite | Definir | Accepted | [T-052](./docs/adrs/T-052-gateway-consumes-core-single-mcp.es.md) | Elimina el servidor MCP del Tracker; el gateway es BFF agregador que consume `core-api` (REST) y `evolith-mcp` (cliente). Supera a `T-051`. Verificado en vivo. |
| T-053 | Consumir la identidad UMS: JWKS/OIDC preferido, simétrico como interino | Definir | Proposed | [T-053](./docs/adrs/T-053-consume-ums-identity.es.md) | El tracker-api valida OIDC/JWKS pero el UMS no lo expone. Preferir que el UMS publique JWKS (aguas arriba); interino simétrico implementado y verificado con mock. |
| T-054 | Gate de frontera en tiempo de edición: adoptar acotado, diferido a EAG-11 | Definir | Accepted | [T-054](./docs/adrs/T-054-edit-time-gate-adoption.es.md) | Adopta el edit-gate del Core pero DIFERIDO: `.claude/` está en `.gitignore`, `EAG-11` aún no da la fuente única de reglas, y el matcher puede misfirear. Activación condicionada a `EAG-11` + versionar `.claude/settings.json` + acotar rutas + vía de escape. |
| T-055 | El Core deposita sus veredictos; el Tracker posee el ledger y deriva el tenant de la clave | Definir | Accepted | [T-055](./docs/adrs/T-055-core-initiated-evidence-ingest.es.md) | Segunda dirección del tráfico de evidencia: `POST /core-evaluation-transactions` autenticado por clave de máquina atada al esquema POR NOMBRE, permiso propio `:ingest` SIN `:read`, tenant derivado de QUÉ CLAVE encajó (un `tenantId` en el cuerpo se rechaza con 400, no se ignora), idempotencia por `(tenant, correlationId)` respaldada por índice único, motor de cada regla VERBATIM (vocabulario abierto) y los dos responsables —quien pidió y quien debe arreglar— en columnas distintas. Estado `ingested`, distinto de `completed`. El DTO derivado a mano cumple `T-038` con guarda de deriva por fixture. Sigue siendo advisory (`T-039`). Cierra `GT-604`. |
2 changes: 2 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,6 @@ _Nota: Las decisiones universales se heredan del Upstream Base ([evolith_arch32]
| T-053 | Consumir la identidad UMS: JWKS/OIDC preferido, simétrico como interino | Definir | Proposed | [T-053](./docs/adrs/T-053-consume-ums-identity.md) | El tracker-api valida OIDC/JWKS pero el UMS no lo expone. Preferir que el UMS publique JWKS (aguas arriba); interino simétrico implementado y verificado con mock. |
| T-054 | Gate de frontera en tiempo de edición: adoptar acotado, diferido a EAG-11 | Definir | Accepted | [T-054](./docs/adrs/T-054-edit-time-gate-adoption.md) | Adopta el edit-gate del Core pero DIFERIDO: `.claude/` está en `.gitignore`, `EAG-11` aún no da la fuente única de reglas, y el matcher puede misfirear. Activación condicionada a `EAG-11` + versionar `.claude/settings.json` + acotar rutas + vía de escape. |

| T-055 | El Core deposita sus veredictos; el Tracker posee el ledger y deriva el tenant de la clave | Definir | Accepted | [T-055](./docs/adrs/T-055-core-initiated-evidence-ingest.md) | Segunda dirección del tráfico de evidencia: `POST /core-evaluation-transactions` autenticado por clave de máquina atada al esquema POR NOMBRE, permiso propio `:ingest` SIN `:read`, tenant derivado de QUÉ CLAVE encajó (un `tenantId` en el cuerpo se rechaza con 400, no se ignora), idempotencia por `(tenant, correlationId)` respaldada por índice único, motor de cada regla VERBATIM (vocabulario abierto) y los dos responsables —quien pidió y quien debe arreglar— en columnas distintas. Estado `ingested`, distinto de `completed`. El DTO derivado a mano cumple `T-038` con guarda de deriva por fixture. Sigue siendo advisory (`T-039`). Cierra `GT-604`. |

> **Plantilla para nuevos ADRs:** Al crear un nuevo documento para "ADR Local", utilice el esquema de Frontmatter definido en los estándares de Evolith Core y ubíquelo en la carpeta de gobernanza correspondiente.
90 changes: 90 additions & 0 deletions contracts/evaluation-ingest.fixture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"_comment": [
"GT-604 — cuerpo canónico de POST /api/v1/core-evaluation-transactions.",
"Conforme a EvaluationIngestPayload de @beyondnet/evolith-contracts/ingest",
"(src/packages/contracts/src/ingest/evaluation-ingest.ts en el repositorio del Core),",
"schemaVersion 1.0.0. NO lleva tenantId: el tenant lo fija la clave de máquina que",
"autentica, y un cuerpo que lo traiga se rechaza con 400.",
"Lo consume CoreEvaluationIngestContractTests, que le da la vuelta a través del DTO de",
"frontera y compara campo a campo: si el DTO deja caer rulesExecuted[].engine o renombra",
"accountableOwner a owner, esa prueba se pone roja aquí en vez de producir un ledger de",
"filas sin motor que nadie mira en un trimestre. Es la guarda que T-038 exige para un",
"binding derivado a mano."
],
"schemaVersion": "1.0.0",
"correlationId": "cli-eval-2026-07-30T10:00:00.000Z",
"producer": {
"surface": "cli",
"version": "evolith-cli@1.2.0"
},
"evaluatedAt": "2026-07-30T10:00:00.000Z",
"overallVerdict": "FAIL",
"outcome": "rejected",
"requestedBy": {
"actorType": "agent",
"actorId": "agent-winston",
"modelRef": "claude-opus-5",
"sessionId": "sesion-604"
},
"repositoryRevision": {
"revision": "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c",
"repositoryRef": "https://github.com/beyondnetcode/evolith_core_demos",
"branch": "develop",
"committedAt": "2026-07-30T09:58:12.000Z",
"dirty": false
},
"rulesExecuted": [
{
"ruleId": "layer-boundary",
"rulesetRef": "hexagonal@1",
"engine": "native",
"verdict": "FAIL"
},
{
"ruleId": "adr-compliance",
"engine": "opa",
"verdict": "PASS"
},
{
"ruleId": "dependency-direction",
"engine": "enforcer",
"verdict": "WARN"
}
],
"violations": [
{
"ruleId": "layer-boundary",
"tool": "dependency-cruiser",
"file": "src/app/handler.ts",
"line": 42,
"column": 7,
"severity": "error",
"message": "la capa de aplicación importa infraestructura",
"adrRef": "ADR-0007",
"accountableOwner": "@beyondnetcode/platform",
"category": "architecture",
"complianceControls": ["SOC2-CC8.1"],
"fingerprint": "9f8e7d6c5b4a3928",
"frozen": false
},
{
"ruleId": "secret-scan",
"tool": "gitleaks",
"file": "src/app/config.ts",
"severity": "error",
"message": "credencial embebida en el fuente",
"accountableOwner": "@beyondnetcode/security",
"category": "security",
"fingerprint": "1a2b3c4d5e6f7081",
"frozen": true
}
],
"accountableOwners": ["@beyondnetcode/platform", "@beyondnetcode/security"],
"blockingViolationCount": 1,
"versions": {
"core": "1.2.0",
"ruleset": "hexagonal",
"rulesetVersion": "1.0.0",
"policy": "evolith-policies@0.9.1"
}
}
121 changes: 121 additions & 0 deletions docs/adrs/T-055-core-initiated-evidence-ingest.es.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
adr: T-055
title: El Core deposita sus veredictos; el Tracker posee el ledger y deriva el tenant de la clave
status: Accepted
date: 2026-07-31
tags: [EvolithSatellite, integration, evidence, security, multi-tenancy]
authority: Evolith Core GT-604 (contrato de ingesta de evidencia), @beyondnet/evolith-contracts/ingest
relates: [T-038 binding por schema neutro, T-045 frontera de conectores, T-039 el Core recomienda y el tenant decide, CD-23 identidad de máquina]
gaps: [GT-604]
---

# ADR T-055 — Ingesta de evidencia iniciada por el Core: el Core deposita, el Tracker posee el ledger

## Status

Accepted (2026-07-31). Registra la decisión detrás del segundo escritor de
`tracker_governance.core_evaluation_transactions`, entregado para la ficha `GT-604` del Core.

## Context

Hasta ahora el camino de escritura de evidencia apuntaba sólo **hacia adentro**. El Tracker abría
transacciones de evaluación contra el Core (`CoreEvaluationGateway` →
`CreateCoreEvaluationTransactionCommand`) y los únicos escritores de
`core_evaluation_transactions` eran esas operaciones que el propio Tracker iniciaba. Las superficies
del Core —`evolith evaluate`, la puerta `--format drift`, la herramienta MCP `evolith-evaluate`,
cada veto de `enforce edit`— producían cada una un veredicto completo con el motor que ejecutó cada
regla, el conjunto de reglas ejecutadas, las violaciones normalizadas y el dueño de cada hallazgo
resuelto por CODEOWNERS, y luego terminaban. El veredicto se perdía.

Eso es un **defecto de composición**: no lo encuentra ninguna revisión de un solo componente, porque
cada componente es correcto por separado. El Core produce buena evidencia; el Tracker la almacena
bien; las pruebas de ninguno de los dos notan que entre ellos no viaja nada. La premisa entera del
producto es la evidencia acumulada, y los componentes que la producen no tenían dónde depositarla.

Tres restricciones dieron forma a la respuesta, y ninguna es estilística:

1. **El tenant no puede venir del cuerpo de la petición.** El Core se autentica como máquina
(`CD-23`, `CoreMachineAuthenticationHandler`), y la clave de máquina es lo que ata a un llamador
con un tenant. Un `tenantId` en el cuerpo dejaría que cualquier clave válida depositara en el
ledger de cualquier tenant — justo el agujero que el handler de máquina existe para cerrar.
2. **El vocabulario de motores es abierto y debe seguir siéndolo.** `RuleExecutionRef.engine` en el
Core crece (`native`, `opa`, `enforcer`, y más a medida que se normalizan analizadores de
código). Una validación del lado del Tracker contra una lista cerrada rechazaría depósitos
válidos o, peor, coaccionaría un motor desconocido a uno conocido — escribiendo una fila que
afirma que una regla de gobernanza produjo un hallazgo que produjo un motor de políticas, una
sustitución que ningún consumidor posterior podría detectar.
3. **Hay DOS responsables y son dos personas distintas.** `requestedBy.actorId` es quien PIDIÓ la
evaluación; `violations[].accountableOwner` es quien debe ARREGLAR el hallazgo. Fundirlos deja
sin respuesta tanto «de qué agente son los veredictos que fallan» como «qué equipo posee los
fallos».

## Decision

**El Core deposita veredictos ya terminados por una única ruta autenticada; el Tracker posee el
ledger, deriva el tenant de la clave y guarda lo que le dieron sin reinterpretarlo.**

En concreto:

1. **Ruta.** `POST /api/v1/core-evaluation-transactions`, autenticada por el esquema `CoreMachine`
atado **por nombre** — de modo que el `dev-bypass` de Development, que autentica cualquier
petición (incluidas las anónimas) con todos los permisos, no la alcanza. Misma disciplina de
vinculación que `POST /runtime-approvals`.
2. **Permiso.** Uno nuevo, `tracker:core-transaction:ingest`, que la clave de máquina lleva **sin**
`tracker:core-transaction:read`. El Core deposita evidencia; no consulta el inventario de
hallazgos, dueños y rutas de fichero del tenant. Una clave filtrada que sólo puede escribir deja
rastro de todo lo que escribe; una que puede leer es un exfiltrador.
3. **Tenant.** Derivado de qué clave encajó. Un cuerpo que traiga `tenantId` se **rechaza con 400**,
nunca se ignora: ignorarlo dejaría al productor creyendo que eligió destino mientras la fila iba
a otro sitio.
4. **Idempotencia.** Por `(tenant_id, correlation_id)`, respaldada por un índice único. Un segundo
depósito actualiza la fila existente y devuelve `200` con `created: false`; nunca devuelve `409`
ni crea una segunda fila. Los reintentos de CI son normales y no pueden contar dos veces un
veredicto. El índice no es una optimización: sin él la regla es inexigible bajo concurrencia y
dos pipelines en carrera escriben dos filas para un mismo veredicto.
5. **Fidelidad.** `rulesExecuted[].engine` se guarda verbatim; los motores desconocidos se toleran y
jamás se coaccionan. Los dos responsables van en columnas distintas y un `accountableOwner`
ausente sigue ausente — un defecto sin atribuir registrado como sin atribuir es honesto; uno
inventado es una acusación.
6. **Estado.** Una fila depositada es `ingested`, no `completed`. `completed` describe una operación
que el Tracker inició y terminó; un depósito no lo inició él y llega ya terminado. Reutilizar el
valor borraría del ledger la distinción entre las dos direcciones del tráfico, que es justo la
distinción que este ADR añade.
7. **Binding.** El DTO de petición es un espejo derivado a mano de `EvaluationIngestPayload`
(`@beyondnet/evolith-contracts/ingest`) — lo que `T-038` permite, porque el paquete es sólo
TypeScript, **a condición** de que viva confinado a la frontera de adaptador y de que un chequeo
falle ante deriva. `contracts/evaluation-ingest.fixture.json` más
`CoreEvaluationIngestContractTests` son ese chequeo: se da la vuelta al fixture a través del DTO
y se compara campo a campo, así que dejar caer `engine` o renombrar `accountableOwner` a `owner`
se pone rojo en el momento del cambio.

## Consequences

**Lo que compra.** «Qué regla, ejecutada por qué motor, produjo qué hallazgo, de quién es, sobre qué
revisión y quién la pidió» pasa a ser una consulta en vez de una línea de consola perdida. Las tres
superficies del Core convergen en una sola forma de cable en lugar de tres mapeos parecidos.

**Lo que cuesta.** `core_evaluation_transactions` sirve ahora a dos propósitos con una sola forma:
una operación que el Tracker inició y un veredicto que el Core depositó. Cada columna de ingesta es
anulable exactamente por eso, y las filas se distinguen por `status`. Si las dos poblaciones
divergen más, partir la tabla es el siguiente paso — no rellenar las filas antiguas, que volvería
«no consta» indistinguible de «consta así».

**Lo que NO cambia.** `T-039` sigue en pie: un veredicto del Core depositado es **advisory**.
Aterrizar en este ledger no satisface un criterio de compuerta, no autoriza una transición de fase y
no constituye una excepción. Es evidencia, y el tenant sigue decidiendo.

**Lo que deliberadamente no se construye.** Ninguna proyección a `EvidenceRecord`: `EAG-17` da a la
evidencia de conector una única puerta de entrada por `ConnectorEvidenceTranslators`, y hacer pasar
un veredicto del Core por un traductor construido para cargas de conector lo distorsionaría o
añadiría un `switch` sin nada en común. Si un veredicto del Core debe convertirse en un
`EvidenceRecord`, eso es un traductor y es su propia decisión.

## Compliance

- `CoreEvaluationIngestEndpointTests` — ruta, vinculación por clave de máquina, tenant desde la
clave, rechazo de `tenantId` en el cuerpo, idempotencia y aislamiento entre tenants. Sin puerta de
escape de entorno: exigen PostgreSQL y fallan si no lo hay.
- `CoreEvaluationIngestSchemaTests` — el índice único y los tipos de columna, contra un motor real.
- `CoreEvaluationIngestContractTests` — la guarda de deriva de `T-038`, verificada por mutación.
- `robosoft/robots/core-evidence-ingest.robot.mjs` — el bucle completo: una ejecución real de
`evolith evaluate` aterriza como fila no vacía y con dueño responsable.
Loading
Loading