Skip to content

GT-604 (Tracker half): open the ledger's front door, and let the Core deposit through it - #82

Merged
beyondnetPeru merged 4 commits into
developfrom
feat/gt-604-evidence-ingest
Jul 31, 2026
Merged

GT-604 (Tracker half): open the ledger's front door, and let the Core deposit through it#82
beyondnetPeru merged 4 commits into
developfrom
feat/gt-604-evidence-ingest

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

The Tracker half of GT-604 (P0) — "No surface writes evidence to the Tracker". Pairs with evolith_arch32#314, which makes the three Core surfaces deposit through it. Also adds T-055.

The gate

POST /api/v1/core-evaluation-transactions, bound to the CoreMachine scheme by name so dev-bypass cannot reach it. A new permission tracker:core-transaction:ingest is carried by the machine key without :read — depositing evidence is not permission to read the ledger. The tenant is derived from which key matched; a body carrying tenantId is rejected with 400, so a caller cannot deposit into someone else's ledger.

Idempotent on (tenant, correlationId), backed by a unique index — without it the idempotency rule is unenforceable under concurrency and two racing CI runs would count the same verdict twice. Engine stored verbatim, both owners in separate columns, status ingested rather than completed.

Evidence, including the negative half

Red first, real output:

Expected response.StatusCode to be OK {200}, but found MethodNotAllowed {405}.
Con error!  Con error: 7, Superado: 0, Total: 7

Then green. Plus CoreEvaluationIngestContractTests (T-038 drift guard), mutation-verified — removing Engine from the DTO yields Campos perdidos: $.rulesExecuted[0].engine, [1].engine, [2].engine — and CoreEvaluationIngestSchemaTests, red against an unmigrated database, green after.

These tests have no environment gates. Unlike the existing CoreEvaluationTransactionPersistenceLiveTests, they fail rather than skip when Postgres is missing or unmigrated. The integrating session confirmed that property by accident: run against a database migrated only to the previous branch, they went 8 red, correctly naming the missing index —

Índices presentes: PK_… | IX_…operation_id | IX_…tenant_id_status, but no such item was found

— and after dotnet ef database update, 12/12 green, with IX_core_evaluation_transactions_tenant_id_correlation_id verified present on the live database. A skip would have hidden that; a failure did not.

Full Tracker suite, Release, -warnaserror: 1033 + 14 passed, 0 failed, 0 warnings. The repo's own cross-language guard caught an omission — new DTO fields required updating tracker-web/src/api/types.ts in the same change.

RoboSoft robot (criterion 3)

robosoft/robots/core-evidence-ingest.robot.mjs scaffolds a failing fixture with a CODEOWNERS, sets two env vars, and runs the real CLI — it posts nothing itself. Against a live tracker-api: 18/18, rules=125, violations=93, owned=93/93, idempotency before=1 after=1. Negative half: with a key the Tracker does not recognise, no row appears and the robot fails at step 4.

Not added to the default CI robot list — it needs the Evolith CLI and a CoreMachine key that pipeline does not deploy, the same reason core-integration and runtime-approvals are excluded. It gets a dedicated local-test.sh robosoft-ingest target that fails loudly instead of skipping.

The declared GT-603 dependency: REFUTED

The row itself flagged it as an unconfirmed hypothesis. It does not hold:

  1. GT-603's diff touches audit_entries only — zero files under Integration/CoreEvaluationTransaction.
  2. core_evaluation_transactions writers emit no audit_entries row, so "ingest writes an audit row as a side effect" is false.
  3. AuditActor is scoped to the Audit aggregate; GT-604's attribution comes from EvaluationResult.requester (Core, GT-586). No shared actor type is forked.
  4. Proof by construction: this was built and fully verified off origin/develop without GT-603.
  5. Merge check: git merge-tree clean; after merging, dotnet ef migrations has-pending-model-changes reports "No changes have been made to the model since the last migration", both migrations apply in sequence, merged suite 1044/1046 (the 2 failures are RepoRoot() returning null because the check worktree lived under /tmp).

It was a transcription error. The rows are independent.

T-055

Warranted and written, bilingual, registered in DECISIONS.md/.es.md and evolith.yaml → adrRegistry: the Core deposits, the Tracker owns the ledger. Bilingual parity green; validate-docs reports the same 111 pre-existing findings as bafebd4, all under docs/audit/, none in these files.

🤖 Generated with Claude Code

beyondnetPeru and others added 4 commits July 30, 2026 22:23
…nes (GT-604)

Hasta ahora los únicos escritores de `core_evaluation_transactions` eran
operaciones que el TRACKER iniciaba contra el Core. Cada `evolith evaluate`,
cada veto de `enforce edit`, cada `tools/call` del MCP y cada pasada del
drift-gate producían un veredicto completo —motor real, reglas ejecutadas,
dueño responsable— que se evaporaba al terminar el proceso.

`POST /api/v1/core-evaluation-transactions` cierra esa mitad:

* el tenant lo fija QUÉ CLAVE de máquina encajó, nunca el cuerpo. Un cuerpo
  que traiga `tenantId` se RECHAZA con 400 en vez de ignorarse: ignorarlo
  dejaría al productor creyendo que eligió destino;
* atado al esquema `CoreMachine` por NOMBRE, de modo que el `dev-bypass` —que
  autentica cualquier petición, incluso anónima, con todos los permisos— no lo
  alcanza ni en local. Permiso propio `tracker:core-transaction:ingest`, que la
  clave lleva SIN `:read`: el Core deposita evidencia, no consulta el inventario
  de hallazgos y dueños del tenant;
* idempotente por `(tenant, correlationId)` con índice único que lo respalda —
  sin él dos pasadas de CI en carrera contarían dos veces un mismo veredicto—;
* el motor de cada regla se guarda VERBATIM: un motor desconocido se tolera,
  jamás se coacciona a `native`, porque esa sustitución sería indetectable para
  todo consumidor posterior;
* los dos responsables siguen siendo dos: `requestedBy.actorId` es quien pidió y
  `violations[].accountableOwner` quien debe arreglar. Un defecto sin dueño se
  registra sin dueño.

Estado `ingested`, distinto de `completed`: un depósito no lo inició el Tracker
y llega ya terminado.

Pruebas, todas SIN puerta de escape de entorno —esta suite ya tiene una clase
que se auto-salta salvo `EVOLITH_CORE_LIVE=1` y reporta verde sin ejecutar:

* `CoreEvaluationIngestEndpointTests` (7) — vistas en rojo con 405/404 antes de
  existir la ruta;
* `CoreEvaluationIngestContractTests` (3) — guarda de deriva que `T-038` exige
  para un binding derivado a mano; verificada por mutación (quitar `Engine` del
  DTO la pone en rojo);
* `CoreEvaluationIngestSchemaTests` (2) — vistas en rojo contra una base sin la
  migración.

`types.ts` y la lista literal de `CoreEvaluationTransactionContractTests` se
actualizan en el mismo cambio, que es justo lo que esa guarda existe para forzar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-604)

`core-evidence-ingest` ejecuta la CLI de Evolith DE VERDAD contra un workspace
que falla y comprueba que el veredicto acaba como fila persistida aquí. El robot
no publica nada: sólo pone `EVOLITH_TRACKER_URL` y `EVOLITH_TRACKER_API_KEY` en
el entorno de la CLI. Si la CLI no está cableada al cliente compartido, no
aparece ninguna fila y el robot falla.

Las afirmaciones que sostienen la prueba son las de NO-VACUIDAD. Un robot que
comprobara sólo «existe una fila» pasaría contra un endpoint que persiste un
sobre vacío, y una fila de ledger vacía es exactamente el modo de fallo del que
trata esta ficha. Se exige: `rulesExecuted` no vacío y con motor en cada regla,
`violations` no vacío, al menos una con `accountableOwner`, que ese dueño sea el
que CODEOWNERS resuelve, y que el solicitante NUNCA se haya escrito como dueño
responsable.

El fixture se genera con un CODEOWNERS que casa con todo, a propósito: sin él
cada hallazgo vuelve «unassigned» y la comprobación del dueño sería vacua —verde
sin demostrar nada—. El `correlationId` se fija en el contexto en vez de dejarlo
a la síntesis de la CLI, para unir por un id que el robot CONOCE y no por «la
fila más reciente», que pasaría contra el depósito de otro.

También comprueba la mitad negativa: un depósito anónimo debe recibir 401. En
Development el `dev-bypass` autentica cualquier petición con todos los permisos,
así que sin la vinculación por nombre de esquema cualquiera podría escribir en el
ledger de evidencia del tenant que quisiera.

Verificado en ambos sentidos contra un tracker-api real: 18/18 en verde con la
clave correcta; con una clave que el Tracker no reconoce el depósito se rechaza,
no aparece fila y el robot FALLA en el paso 4.

NO se añade a la lista por defecto de `local-test.sh robosoft`, por la misma
razón que dejan fuera a `core-integration` y `runtime-approvals`: necesita la CLI
de Evolith y una clave CoreMachine ligada al tenant que el robot lee, y ninguna
de las dos las despliega ese pipeline. Meterlo sin ellas pintaría la puerta de
rojo por un motivo de entorno y no por un defecto, que es la forma más rápida de
enseñar a un equipo a ignorar una puerta. Va en un objetivo propio,
`robosoft-ingest`, que FALLA en voz alta si falta la CLI en vez de saltarse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La ingesta de evidencia añade un segundo escritor a una tabla que ya existía, un
permiso nuevo a la clave de máquina y un valor de estado nuevo. Eso es una
decisión de frontera, no un detalle de implementación, y este satélite registra
sus decisiones locales como `T-NNN`: sin ADR, la siguiente persona que mire
`core_evaluation_transactions` no puede saber por qué las columnas de ingesta son
anulables ni por qué `ingested` no es `completed`.

Lo que el ADR fija, y por qué en cada caso:

* el tenant sale de QUÉ CLAVE encajó y un `tenantId` en el cuerpo se rechaza con
  400 en vez de ignorarse;
* `:ingest` sin `:read` — el Core deposita evidencia, no consulta el inventario
  de hallazgos, dueños y rutas del tenant;
* el motor de cada regla, verbatim: el vocabulario del Core es abierto y
  coaccionarlo escribiría una afirmación falsa que nadie podría detectar después;
* los dos responsables en columnas distintas;
* el índice único no es una optimización: sin él la idempotencia es inexigible
  bajo concurrencia;
* sigue siendo advisory (`T-039`): aterrizar aquí no satisface ninguna compuerta.

Registrado en `DECISIONS.md`/`.es.md` y en `evolith.yaml → adrRegistry`, con
gemelo bilingüe. `check-bilingual-parity` en verde; `validate-docs` reporta los
mismos 111 hallazgos que en `bafebd4`, todos en `docs/audit/`, ninguno de estos
ficheros.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three files conflicted with GT-615, and only two of them were the additive kind
where keeping both sides is correct (a new repository method beside a new one).

The third was not, and the build caught it: `GET /` had a stale pre-GT-615 body
on this branch, so a naive both-sides resolution declared `items` twice. Taking
develop's handler wholesale then went green on compile and RED on four ingest
tests — `Expected despues.Count(r => r.CorrelationId == correlationId) to be 1,
but found 0`. The cause was not the handler: develop's private `ToDto` is an
older, narrower copy of `CoreEvaluationTransactionDto.From`, and it does not
carry `CorrelationId` at all. A merge that compiled would have silently dropped
the field the whole ingest contract is keyed on.

Resolved by deleting the private duplicate and mapping through
`CoreEvaluationTransactionDto.From` — the complete mapper in the Application
layer, and now the only one.

Tracker.Tests 1055/1055, architecture tests green, Release build clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru
beyondnetPeru merged commit c05ac46 into develop Jul 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant